171 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Ruby
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			171 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Ruby
		
	
	
		
			Executable File
		
	
	
| #!/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 if filename !~ /\.rb\Z/
 | |
| 
 | |
|       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 line =~ /#{CODE_LINES_SEARCH_STRING}/o
 | |
| 
 | |
|         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
 |