Skip to content

Commit cc01d8e

Browse files
authored
Merge pull request #1 from reclaim-the-stack/diff-handling
Add local diff handling
2 parents 1cfffe6 + 89b4fce commit cc01d8e

File tree

2 files changed

+99
-62
lines changed

2 files changed

+99
-62
lines changed

lib/github.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
require "json"
2+
require "net/http"
3+
4+
module Github
5+
HttpError = Class.new(StandardError)
6+
7+
class File
8+
RANGE_INFORMATION_LINE = /^@@ .+\+(?<line_number>\d+),/.freeze
9+
MODIFIED_LINE = /^\+(?!\+|\+)/.freeze
10+
NOT_REMOVED_LINE = /^[^-]/.freeze
11+
12+
def initialize(file)
13+
@file = file
14+
end
15+
16+
def path
17+
@file.fetch("filename")
18+
end
19+
20+
def changed_lines
21+
patch = @file.fetch("patch") || ""
22+
line_number = 0
23+
patch.each_line.with_object([]) do |line_content, lines|
24+
case line_content
25+
when RANGE_INFORMATION_LINE
26+
line_number = Regexp.last_match[:line_number].to_i
27+
when MODIFIED_LINE
28+
lines << line_number
29+
line_number += 1
30+
when NOT_REMOVED_LINE
31+
line_number += 1
32+
end
33+
end
34+
end
35+
end
36+
37+
CONNECTION = Net::HTTP.new("api.github.com", 443).tap { |http| http.use_ssl = true }
38+
REQUEST_METHOD_TO_CLASS = {
39+
get: Net::HTTP::Get,
40+
patch: Net::HTTP::Patch,
41+
post: Net::HTTP::Post,
42+
delete: Net::HTTP::Delete,
43+
}.freeze
44+
45+
REQUEST_METHOD_TO_CLASS.each do |method, klass|
46+
define_singleton_method(method) do |path, params = nil|
47+
request(klass, path, params)
48+
end
49+
50+
define_singleton_method("#{method}!") do |path, params = nil|
51+
response = request(klass, path, params)
52+
raise HttpError, "status: #{response.code}, body: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
53+
54+
JSON.parse(response.body) if response.body
55+
end
56+
end
57+
58+
def self.pull_request_ruby_files(owner_and_repository, pr_number)
59+
changed_files = []
60+
1.step do |page|
61+
files = Github.get!("/repos/#{owner_and_repository}/pulls/#{pr_number}/files?per_page=100&page=#{page}")
62+
changed_files.concat(files)
63+
break if files.length < 100
64+
end
65+
changed_files
66+
.reject { |file| file.fetch("status") == "removed" }
67+
.select { |file| file.fetch("filename").end_with?(".rb") }
68+
.map { |file| File.new(file) }
69+
end
70+
71+
def self.request(request_class, path, params = nil)
72+
request = request_class.new(path)
73+
request.content_type = "application/json"
74+
request.body = params&.to_json
75+
request["Authorization"] = "Bearer #{ENV.fetch('GITHUB_TOKEN')}"
76+
request["Accept"] = "application/vnd.github.v3+json"
77+
78+
CONNECTION.request(request)
79+
end
80+
end

rubocop.rb

Lines changed: 19 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -23,70 +23,23 @@
2323

2424
# Script
2525

26-
require "json"
27-
require "net/http"
28-
29-
module Github
30-
HttpError = Class.new(StandardError)
31-
32-
CONNECTION = Net::HTTP.new("api.github.com", 443).tap { |http| http.use_ssl = true }
33-
REQUEST_METHOD_TO_CLASS = {
34-
get: Net::HTTP::Get,
35-
patch: Net::HTTP::Patch,
36-
post: Net::HTTP::Post,
37-
delete: Net::HTTP::Delete,
38-
}.freeze
39-
40-
REQUEST_METHOD_TO_CLASS.each do |method, klass|
41-
define_singleton_method(method) do |path, params = nil|
42-
request(klass, path, params)
43-
end
44-
45-
define_singleton_method("#{method}!") do |path, params = nil|
46-
response = request(klass, path, params)
47-
raise HttpError, "status: #{response.code}, body: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
48-
49-
JSON.parse(response.body) if response.body
50-
end
51-
end
52-
53-
def self.request(request_class, path, params = nil)
54-
request = request_class.new(path)
55-
request.content_type = "application/json"
56-
request.body = params&.to_json
57-
request["Authorization"] = "Bearer #{ENV.fetch('GITHUB_TOKEN')}"
58-
request["Accept"] = "application/vnd.github.v3+json"
59-
60-
CONNECTION.request(request)
61-
end
62-
end
26+
require_relative "lib/github"
6327

6428
# Figure out which ruby files have changed and run Rubocop on them
6529

6630
github_event = JSON.parse(File.read(ENV.fetch("GITHUB_EVENT_PATH")))
6731
pr_number = github_event.fetch("pull_request").fetch("number")
6832
owner_and_repository = ENV.fetch("GITHUB_REPOSITORY")
6933

70-
changed_files = []
71-
1.step do |page|
72-
files = Github.get!("/repos/#{owner_and_repository}/pulls/#{pr_number}/files?per_page=100&page=#{page}")
73-
changed_files.concat(files)
74-
break if files.length < 100
75-
end
76-
changed_ruby_files = changed_files
77-
.reject { |file| file.fetch("status") == "removed" }
78-
.select { |file| file.fetch("filename").end_with?(".rb") }
79-
.map { |file| file.fetch("filename") }
34+
changed_ruby_files = Github.pull_request_ruby_files(owner_and_repository, pr_number)
8035

8136
# JSON reference: https://docs.rubocop.org/rubocop/formatters.html#json-formatter
8237
files_with_offenses =
8338
if changed_ruby_files.any?
84-
command = "rubocop #{changed_ruby_files.join(' ')} --format json --force-exclusion"
39+
command = "rubocop #{changed_ruby_files.map(&:path).join(' ')} --format json --force-exclusion"
8540

8641
puts "Running rubocop with: #{command}"
87-
rubocop_output = `#{command}`
88-
89-
JSON.parse(rubocop_output).fetch("files")
42+
JSON.parse(`#{command}`).fetch("files")
9043
else
9144
puts "No changed Ruby files, skipping rubocop"
9245

@@ -126,6 +79,11 @@ def self.request(request_class, path, params = nil)
12679

12780
# Comment on the pull request with the offenses found
12881

82+
def in_diff?(changed_files, path, line)
83+
file = changed_files.find { |changed_file| changed_file.path == path }
84+
file&.changed_lines&.include?(line)
85+
end
86+
12987
offences_outside_diff = []
13088

13189
files_with_offenses.each do |file|
@@ -166,26 +124,21 @@ def self.request(request_class, path, params = nil)
166124

167125
puts "Updating comment #{comment_id} on #{path} line #{line}"
168126
Github.patch("/repos/#{owner_and_repository}/pulls/comments/#{comment_id}", body: body)
169-
else
127+
elsif in_diff?(changed_ruby_files, path, line)
170128
puts "Commenting on #{path} line #{line}"
171129

172130
# Somehow the commit_id should not be just the HEAD SHA: https://stackoverflow.com/a/71431370/1075108
173131
commit_id = github_event.fetch("pull_request").fetch("head").fetch("sha")
174132

175-
response = Github.post(
133+
Github.post!(
176134
"/repos/#{owner_and_repository}/pulls/#{pr_number}/comments",
177135
body: body,
178136
path: path,
179137
commit_id: commit_id,
180138
line: line,
181139
)
182-
183-
# Rubocop might hit errors on lines which are not part of the diff and thus cannot be commented on.
184-
if response.code == "422" && response.body.include?("line must be part of the diff")
185-
puts "Deferring comment on #{path} line #{line} because it isn't part of the diff"
186-
187-
offences_outside_diff << { path: path, line: line, message: message }
188-
end
140+
else
141+
offences_outside_diff << { path: path, line: line, message: message }
189142
end
190143
end
191144
end
@@ -196,7 +149,9 @@ def self.request(request_class, path, params = nil)
196149
existing_separate_comment = separate_comments.find do |comment|
197150
comment.fetch("body").include?("rubocop-comment-id: outside-diff")
198151
end
152+
199153
if offences_outside_diff.any?
154+
puts "Found #{offences_outside_diff.count} offenses outside of the diff"
200155

201156
body = <<~BODY
202157
<!-- rubocop-comment-id: outside-diff -->
@@ -215,8 +170,8 @@ def self.request(request_class, path, params = nil)
215170
if existing_separate_comment.fetch("body") == body
216171
puts "Skipping unchanged separate comment #{existing_comment_id}"
217172
else
218-
puts "Updating comment #{existing_comment_id} on pull request"
219-
Github.patch!("/repos/#{owner_and_repository}/pulls/comments/#{existing_comment_id}", body: body)
173+
puts "Updating separate comment #{existing_comment_id}"
174+
Github.patch!("/repos/#{owner_and_repository}/issues/comments/#{existing_comment_id}", body: body)
220175
end
221176
else
222177
puts "Commenting on pull request with offenses found outside the diff"
@@ -227,6 +182,8 @@ def self.request(request_class, path, params = nil)
227182
existing_comment_id = existing_separate_comment.fetch("id")
228183
puts "Deleting resolved separate comment #{existing_comment_id}"
229184
Github.delete("/repos/#{owner_and_repository}/issues/comments/#{existing_comment_id}")
185+
else
186+
puts "No offenses found outside of the diff and no existing separate comment to remove"
230187
end
231188

232189
# Fail the build if there were any offenses

0 commit comments

Comments
 (0)