class Gitlab::Triage::Engine

Constants

ALLOWED_STATE_VALUES
DEFAULT_GRAPHQL_ADAPTER
DEFAULT_NETWORK_ADAPTER
EpicsTriagingForProjectImpossibleError

Attributes

options[R]
per_page[R]
policies[R]

Public Class Methods

new(policies:, options:, network_adapter_class: DEFAULT_NETWORK_ADAPTER, graphql_network_adapter_class: DEFAULT_GRAPHQL_ADAPTER) click to toggle source
# File lib/gitlab/triage/engine.rb, line 42
def initialize(policies:, options:, network_adapter_class: DEFAULT_NETWORK_ADAPTER, graphql_network_adapter_class: DEFAULT_GRAPHQL_ADAPTER)
  options.host_url = policies.delete(:host_url) { options.host_url }
  options.api_version = policies.delete(:api_version) { 'v4' }
  options.dry_run = ENV['TEST'] == 'true' if options.dry_run.nil?

  @per_page = policies.delete(:per_page) { 100 }
  @policies = policies
  @options = options
  @network_adapter_class = network_adapter_class
  @graphql_network_adapter_class = graphql_network_adapter_class

  assert_all!
  assert_project_id!
  assert_token!
  require_ruby_files
end

Public Instance Methods

graphql_network() click to toggle source
# File lib/gitlab/triage/engine.rb, line 82
def graphql_network
  @graphql_network ||= GraphqlNetwork.new(graphql_network_adapter)
end
network() click to toggle source
# File lib/gitlab/triage/engine.rb, line 78
def network
  @network ||= Network.new(network_adapter)
end
perform() click to toggle source
# File lib/gitlab/triage/engine.rb, line 59
def perform
  puts "Performing a dry run.\n\n" if options.dry_run

  puts Gitlab::Triage::UI.header("Triaging the `#{options.source_id}` #{options.source.to_s.singularize}", char: '=')
  puts

  resource_rules.each do |resource_type, resource|
    if resource_type == 'epics' && options.source != :groups
      raise(EpicsTriagingForProjectImpossibleError, "Epics can only be triaged at the group level. Please set the `--source groups` option.")
    end

    puts Gitlab::Triage::UI.header("Processing rules for #{resource_type}", char: '-')
    puts

    process_summaries(resource_type, resource[:summaries])
    process_rules(resource_type, resource[:rules])
  end
end

Private Instance Methods

assert_all!() click to toggle source
# File lib/gitlab/triage/engine.rb, line 101
def assert_all!
  raise ArgumentError, '--all-projects option cannot be used in conjunction with --source and --source-id option!' if
    options.all && (options.source || options.source_id)
end
assert_project_id!() click to toggle source
# File lib/gitlab/triage/engine.rb, line 88
def assert_project_id!
  return if options.source_id
  return if options.all

  raise ArgumentError, 'A project_id is needed (pass it with the `--source-id` option)!'
end
assert_token!() click to toggle source
# File lib/gitlab/triage/engine.rb, line 95
def assert_token!
  return if options.token

  raise ArgumentError, 'A token is needed (pass it with the `--token` option)!'
end
attach_resource_type(resources, resource_type) click to toggle source
# File lib/gitlab/triage/engine.rb, line 216
def attach_resource_type(resources, resource_type)
  resources.each { |resource| resource[:type] = resource_type }
  # TODO:                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  # We should not overwrite the attribute here, but we need to
  # fix it first. We should instead use something like
  # gitlab_triage_resource_type so it won't conflict with the
  # existing fields.
  # And we need to retain the backward compatibility that using
  # {{type}} will give us this value, rather than from the REST API,
  # which will give us ISSUE from:
  # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59648
end
build_get_url(resource_type, conditions) click to toggle source
# File lib/gitlab/triage/engine.rb, line 290
def build_get_url(resource_type, conditions)
  # Example issues query with state and labels
  # https://gitlab.com/api/v4/projects/test-triage%2Fissue-project/issues?state=open&labels=project%20label%20with%20spaces,group_label_no_spaces
  params = {
    per_page: per_page
  }

  condition_builders = []
  condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('labels', conditions[:labels], ',') if conditions[:labels]

  if conditions[:forbidden_labels]
    condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('not[labels]', conditions[:forbidden_labels], ',')
  end

  if conditions[:state]
    condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new(
      'state',
      conditions[:state],
      allowed_values: ALLOWED_STATE_VALUES[resource_type])
  end

  condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('milestone', Array(conditions[:milestone])[0]) if conditions[:milestone]
  condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('source_branch', conditions[:source_branch]) if conditions[:source_branch]
  condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('target_branch', conditions[:target_branch]) if conditions[:target_branch]

  if conditions[:date] && APIQueryBuilders::DateQueryParamBuilder.applicable?(conditions[:date])
    condition_builders << APIQueryBuilders::DateQueryParamBuilder.new(conditions.delete(:date))
  end

  if conditions[:weight] && resource_type.to_sym == :issues
    condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('weight', conditions[:weight])
  end

  condition_builders.each do |condition_builder|
    params[condition_builder.param_name] = condition_builder.param_content
  end

  UrlBuilders::UrlBuilder.new(
    network_options: options,
    all: options.all,
    source: options.source,
    source_id: options.source_id,
    resource_type: resource_type,
    params: params
  ).build
end
build_graphql_query(resource_type, conditions, graphql_only = false) click to toggle source
# File lib/gitlab/triage/engine.rb, line 337
def build_graphql_query(resource_type, conditions, graphql_only = false)
  Gitlab::Triage::GraphqlQueries::QueryBuilder
    .new(options.source, resource_type, conditions, graphql_only: graphql_only)
end
decorate_resources_with_graphql_data(resources, graphql_resources) click to toggle source
# File lib/gitlab/triage/engine.rb, line 229
def decorate_resources_with_graphql_data(resources, graphql_resources)
  return if graphql_resources.nil?

  graphql_resources_by_id = graphql_resources.to_h { |resource| [resource[:id], resource] }
  resources.each { |resource| resource.merge!(graphql_resources_by_id[resource[:id]].to_h) }
end
fetch_source_full_path() click to toggle source
# File lib/gitlab/triage/engine.rb, line 346
def fetch_source_full_path
  return options.source_id unless /\A\d+\z/.match?(options.source_id)

  source_details = network.query_api(build_get_url(nil, {})).first
  full_path = source_details['full_path'] || source_details['path_with_namespace']

  raise ArgumentError, 'A source with given source_id was not found!' if full_path.blank?

  full_path
end
filter_resources(resources, conditions) click to toggle source
# File lib/gitlab/triage/engine.rb, line 244
def filter_resources(resources, conditions)
  resources.select do |resource|
    results = []

    # rubocop:disable Style/IfUnlessModifier
    if conditions[:date]
      results << Filters::MergeRequestDateConditionsFilter.new(resource, conditions[:date]).calculate
    end

    if conditions[:upvotes]
      results << Filters::VotesConditionsFilter.new(resource, conditions[:upvotes]).calculate
    end

    if conditions[:no_additional_labels]
      results << Filters::NoAdditionalLabelsConditionsFilter.new(resource, conditions.fetch(:labels) { [] }).calculate
    end

    if conditions[:author_member]
      results << Filters::AuthorMemberConditionsFilter.new(resource, conditions[:author_member], network).calculate
    end

    if conditions[:assignee_member]
      results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member], network).calculate
    end

    if conditions[:discussions]
      results << Filters::DiscussionsConditionsFilter.new(resource, conditions[:discussions]).calculate
    end

    if conditions[:ruby]
      results << Filters::RubyConditionsFilter.new(resource, conditions, network).calculate
    end
    # rubocop:enable Style/IfUnlessModifier

    results.all?
  end
end
graphql_network_adapter() click to toggle source
# File lib/gitlab/triage/engine.rb, line 118
def graphql_network_adapter
  @graphql_network_adapter ||= @graphql_network_adapter_class.new(options)
end
limit_resources(resources, limits) click to toggle source
# File lib/gitlab/triage/engine.rb, line 282
def limit_resources(resources, limits)
  if limits.empty?
    resources
  else
    Limiters::DateFieldLimiter.new(resources, limits).limit
  end
end
network_adapter() click to toggle source
# File lib/gitlab/triage/engine.rb, line 114
def network_adapter
  @network_adapter ||= @network_adapter_class.new(options)
end
process_action(policy) click to toggle source
# File lib/gitlab/triage/engine.rb, line 236
def process_action(policy)
  Action.process(
    policy: policy,
    network: network,
    dry: options.dry_run)
  puts
end
process_rules(resource_type, rules) click to toggle source
# File lib/gitlab/triage/engine.rb, line 138
def process_rules(resource_type, rules)
  return if rules.blank?

  rules.each do |rule|
    resources_for_rule(resource_type, rule) do |resources|
      policy = Policies::RulePolicy.new(
        resource_type, rule, resources, network)

      process_action(policy)
    end
  end
end
process_summaries(resource_type, summaries) click to toggle source
# File lib/gitlab/triage/engine.rb, line 130
def process_summaries(resource_type, summaries)
  return if summaries.blank?

  summaries.each do |summary|
    process_summary(resource_type, summary)
  end
end
process_summary(resource_type, summary) click to toggle source
# File lib/gitlab/triage/engine.rb, line 151
def process_summary(resource_type, summary)
  puts Gitlab::Triage::UI.header("Processing summary: **#{summary[:name]}**", char: '~')
  puts

  summary_parts_for_rules(resource_type, summary[:rules]) do |resources|
    policy = Policies::SummaryPolicy.new(
      resource_type, summary, resources, network)

    process_action(policy)
  end
end
require_ruby_files() click to toggle source
# File lib/gitlab/triage/engine.rb, line 106
def require_ruby_files
  options.require_files.each(&method(:require))
end
resource_rules() click to toggle source
# File lib/gitlab/triage/engine.rb, line 110
def resource_rules
  @resource_rules ||= policies.delete(:resource_rules) { {} }
end
resources_for_rule(resource_type, rule) { |rule_resources, conditions| ... } click to toggle source
# File lib/gitlab/triage/engine.rb, line 179
def resources_for_rule(resource_type, rule)
  puts Gitlab::Triage::UI.header("Gathering resources for rule: **#{rule[:name]}**", char: '-')

  ExpandCondition.perform(rule_conditions(rule)) do |conditions|
    # retrieving the resources for every rule is inefficient
    # however, previous rules may affect those upcoming
    resources = []

    if rule[:api] == 'graphql'
      graphql_query = build_graphql_query(resource_type, conditions, true)
      resources = graphql_network.query(graphql_query, source: source_full_path)
    else
      resources = network.query_api(build_get_url(resource_type, conditions))
      iids = resources.pluck('iid').map(&:to_s)

      graphql_query = build_graphql_query(resource_type, conditions)
      graphql_resources = graphql_network.query(graphql_query, source: source_full_path, iids: iids) if graphql_query.any?

      decorate_resources_with_graphql_data(resources, graphql_resources)
    end

    # In some filters/actions we want to know which resource type it is
    attach_resource_type(resources, resource_type)

    puts "\n\n* Found #{resources.count} resources..."
    print "* Filtering resources..."
    resources = filter_resources(resources, conditions)
    puts "\n* Total after filtering: #{resources.count} resources"
    print "* Limiting resources..."
    resources = limit_resources(resources, rule_limits(rule))
    puts "\n* Total after limiting: #{resources.count} resources"
    puts

    yield(PoliciesResources::RuleResources.new(resources), conditions)
  end
end
rule_conditions(rule) click to toggle source
# File lib/gitlab/triage/engine.rb, line 122
def rule_conditions(rule)
  rule.fetch(:conditions) { {} }
end
rule_limits(rule) click to toggle source
# File lib/gitlab/triage/engine.rb, line 126
def rule_limits(rule)
  rule.fetch(:limits) { {} }
end
source_full_path() click to toggle source
# File lib/gitlab/triage/engine.rb, line 342
def source_full_path
  @source_full_path ||= fetch_source_full_path
end
summary_parts_for_rules(resource_type, rules) { |summary_resources| ... } click to toggle source
# File lib/gitlab/triage/engine.rb, line 163
def summary_parts_for_rules(resource_type, rules)
  # { summary_rule => resources }
  parts = rules.inject({}) do |result, rule|
    resources_and_conditions =
      to_enum(:resources_for_rule, resource_type, rule)

    resources_and_conditions
      .inject(result) do |result, (resources, conditions)|
        # { expanded_summary_rule => resources }
        result.merge(rule.merge(conditions: conditions) => resources)
      end
  end

  yield(PoliciesResources::SummaryResources.new(parts))
end