1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
|
#!/usr/bin/env ruby
# frozen_string_literal: true
####
# Prints a report which helps reconcile occurrences of the `QueryLimiting.disable(ISSUE_LINK)`
# allowlist block against the corresponding open issues.
#
# If everything is consistent, the script should ideally not report any issues or code lines,
# other than possibly remaining "calls with no issue iid" which use variables/etc.
#
# - See https://gitlab.com/gitlab-org/gitlab/-/issues/325640
# - See https://gitlab.com/groups/gitlab-org/-/epics/5670
# We need to take some precautions when using the `gitlab` gem in this project.
#
# See https://docs.gitlab.com/ee/development/pipelines/internals.html#using-the-gitlab-ruby-gem-in-the-canonical-project.
require 'gitlab'
require 'optparse'
require 'rubygems'
class QueryLimitingReport
GITLAB_PROJECT_ID = 278964 # gitlab-org/gitlab project
ISSUES_SEARCH_LABEL = 'querylimiting-disable'
CODE_LINES_SEARCH_STRING = 'QueryLimiting.disable'
PAGINATION_LIMIT = 500
DEFAULT_OPTIONS = {
api_token: ENV['API_TOKEN']
}.freeze
def initialize(options)
@options = options
Gitlab.configure do |config|
config.endpoint = 'https://gitlab.com/api/v4'
config.private_token = options.fetch(:api_token)
end
end
def execute
# PLAN:
# Read all issues matching criteria and extract array of issue iids
# Find all code references and extract issue iids
# Print list of all issues without code references
# Print list of all code references issue iids that don't have search label
# Print list of all code references with no issue iids (i.e. dynamic or variable argument)
total_issues = find_issues_by_label(ISSUES_SEARCH_LABEL)
issues = total_issues.select { |issue| issue[:state] == 'opened' }
code_lines = find_code_lines
code_lines_grouped = code_lines.group_by { |code_line| code_line[:has_issue_iid] }
code_lines_without_issue_iid = code_lines_grouped[false]
code_lines_with_issue_iid = code_lines_grouped[true]
all_issue_iids_in_code_lines = code_lines_with_issue_iid.map { |line| line[:issue_iid] }
issues_without_code_references = issues.reject do |issue|
all_issue_iids_in_code_lines.include?(issue[:iid])
end
all_issue_iids = issues.map { |issue| issue[:iid] }
code_lines_with_missing_issues = code_lines_with_issue_iid.reject do |code_line|
all_issue_iids.include?(code_line[:issue_iid])
end
puts "\n\n\nREPORT:"
puts "\n\nFound #{total_issues.length} total issues with '#{ISSUES_SEARCH_LABEL}' search label, #{issues.length} are still opened..."
puts "\n\nFound #{code_lines.length} total occurrences of '#{CODE_LINES_SEARCH_STRING}' in code..."
puts "\n" + ('-' * 80)
puts "\n\nIssues without any '#{CODE_LINES_SEARCH_STRING}' code references (#{issues_without_code_references.length} total):"
pp issues_without_code_references
puts "\n" + ('-' * 80)
puts "\n\n'#{CODE_LINES_SEARCH_STRING}' calls with references to an issue which doesn't have '#{ISSUES_SEARCH_LABEL}' search label (#{code_lines_with_missing_issues.length} total):"
pp code_lines_with_missing_issues
puts "\n" + ('-' * 80)
puts "\n\n'#{CODE_LINES_SEARCH_STRING}' calls with no issue iid (#{code_lines_without_issue_iid&.length || 0} total):"
pp code_lines_without_issue_iid
end
private
attr_reader :options
def find_issues_by_label(label)
issues = []
puts("Finding issues by label #{label}...")
paginated_issues = Gitlab.issues(GITLAB_PROJECT_ID, 'labels' => label)
paginated_issues.paginate_with_limit(PAGINATION_LIMIT) do |item|
item_hash = item.to_hash
issue_iid = item_hash.fetch('iid')
issue = {
iid: issue_iid,
state: item_hash.fetch('state'),
title: item_hash.fetch('title'),
issue_url: "https://gitlab.com/gitlab-org/gitlab/issues/#{issue_iid}"
}
issues << issue
end
issues
end
def find_code_lines
code_lines = []
puts("Finding code lines...")
paginated_blobs = Gitlab.search_in_project(GITLAB_PROJECT_ID, 'blobs', CODE_LINES_SEARCH_STRING)
paginated_blobs.paginate_with_limit(PAGINATION_LIMIT) do |item|
item_hash = item.to_hash
filename = item_hash.fetch('filename')
next unless /\.rb\Z/.match?(filename)
file_contents = Gitlab.file_contents(GITLAB_PROJECT_ID, filename)
file_lines = file_contents.split("\n")
file_lines.each_index do |index|
line = file_lines[index]
next unless /#{CODE_LINES_SEARCH_STRING}/o.match?(line)
issue_iid = line.slice(%r{issues/(\d+)\D}, 1)
line_number = index + 1
code_line = {
file_location: "#{filename}:#{line_number}",
filename: filename,
line_number: line_number,
line: line,
issue_iid: issue_iid.to_i,
has_issue_iid: !issue_iid.nil?
}
code_lines << code_line
end
end
code_lines.sort_by! { |line| "#{line[:filename]}-#{line[:line_number].to_s.rjust(4, '0')}" }
code_lines.map do |line|
line.delete(:filename)
line.delete(:line_number)
line
end
end
end
if $PROGRAM_NAME == __FILE__
options = QueryLimitingReport::DEFAULT_OPTIONS.dup
OptionParser.new do |opts|
opts.on("-t", "--api-token API_TOKEN", String, "A value API token with the `read_api` scope. Can be set as an env variable 'API_TOKEN'.") do |value|
options[:api_token] = value
end
opts.on("-h", "--help", "Prints this help") do
puts opts
exit
end
end.parse!
QueryLimitingReport.new(options).execute
end
|