ManageIQ/miq_bot

View on GitHub
app/workers/commit_monitor.rb

Summary

Maintainability
A
3 hrs
Test Coverage
require 'yaml'

class CommitMonitor
  include Sidekiq::Worker
  sidekiq_options :queue => :miq_bot_glacial, :retry => false

  include SidekiqWorkerMixin

  # commit handlers expect to handle a specific commit at a time.
  #
  # Example: A commit message checker that will check for URLs and act upon them.
  def self.commit_handlers
    @commit_handlers ||= handlers_for(:commit)
  end

  # commit_range handlers expect to handle a range of commits as a group.
  #
  # Example: A style/syntax/warning checker on a PR branch, where we only want
  #   to check the new commits, but as a group, since newer commits may fix
  #   issues in prior commits.
  def self.commit_range_handlers
    @commit_range_handlers ||= handlers_for(:commit_range).select { |h| !h.respond_to?(:perform_batch_async) }
  end

  # branch handlers expect to handle an entire branch at once.
  #
  # Example: A PR branch mergability tester to see if the entire branch can be
  #   merged or not.
  def self.branch_handlers
    @branch_handlers ||= handlers_for(:branch)
  end

  # batch handlers expect to handle a batch of workers at once and will need
  #   a wider range of information
  #
  # Example: A general commenter to GitHub for a number of issues
  def self.batch_handlers
    @batch_handlers ||= handlers_for(:commit_range).select { |h| h.respond_to?(:perform_batch_async) }
  end

  def perform
    if !first_unique_worker?
      logger.info "#{self.class} is already running, skipping"
    else
      process_repos
    end
  end

  def process_repos
    enabled_repos.includes(:branches).each { |repo| process_repo(repo) }
  end

  private

  attr_reader :repo, :branch, :new_commits, :all_commits, :statistics

  def process_repo(repo)
    @statistics = {}

    @repo = repo
    repo.git_fetch

    # Sort PR branches after regular branches
    sorted_branches = repo.branches.sort_by { |b| b.pull_request? ? 1 : -1 }

    sorted_branches.each do |branch|
      @new_commits_details = nil
      @branch = branch
      process_branch
    end
  end

  def process_branch
    logger.info "Processing #{repo.name}/#{branch.name}"

    @new_commits, @all_commits = detect_commits

    statistics[branch.name] = {:new_commits => new_commits} unless branch.pull_request?

    logger.info "Detected new commits #{new_commits}" if new_commits.any?

    save_branch_record
    process_handlers
  end

  def detect_commits
    send("detect_commits_on_#{branch.mode}_branch")
  end

  def detect_commits_on_regular_branch
    return branch.git_service.commit_ids_since(branch.last_commit), nil
  end

  def detect_commits_on_pr_branch
    all        = branch.git_service.commit_ids_since(branch.git_service.merge_base)
    comparison = compare_commits_list(branch.commits_list, all)
    return comparison[:right_only], all
  end

  def compare_commits_list(left, right)
    return {:same => left.dup, :left_only => [], :right_only => []} if left == right

    combined = left.zip_stretched(right)
    pivot    = combined.index { |c1, c2| c1 != c2 } || -1

    same = left[0...pivot]
    left_only, right_only = combined[pivot..-1].transpose.collect(&:compact)

    {:same => same, :left_only => left_only, :right_only => right_only}
  end

  def new_commits_details
    @new_commits_details ||=
      new_commits.each_with_object({}) do |commit, h|
        h[commit] = branch.git_service.commit(commit).details_hash
      end
  end

  def save_branch_record
    attrs = {:last_checked_on => Time.now.utc}
    attrs[:last_commit] = new_commits.last if new_commits.any?

    if all_commits != branch.commits_list
      attrs[:commits_list] = all_commits
    end

    # Update columns directly to avoid collisions with other workers.  See: https://github.com/rails/rails/issues/8328
    branch.update_columns(attrs)
  end

  #
  # Handler processing methods
  #

  def self.handlers_for(type)
    workers_path = Rails.root.join("app/workers")
    Dir.glob(workers_path.join("commit_monitor_handlers/#{type}/*.rb")).collect do |f|
      path = Pathname.new(f).relative_path_from(workers_path).to_s
      path.chomp(".rb").classify.constantize
    end
  end
  private_class_method(:handlers_for)

  def filter_handlers(handlers)
    handlers.select do |h|
      h.handled_branch_modes.include?(branch.mode) && h.enabled_for?(repo)
    end
  end

  def commit_handlers
    filter_handlers(self.class.commit_handlers)
  end

  def commit_range_handlers
    filter_handlers(self.class.commit_range_handlers)
  end

  def branch_handlers
    filter_handlers(self.class.branch_handlers)
  end

  def batch_handlers
    filter_handlers(self.class.batch_handlers)
  end

  def process_handlers
    process_commit_handlers       if process_commit_handlers?
    process_commit_range_handlers if process_commit_range_handlers?
    process_branch_handlers       if process_branch_handlers?
    process_batch_handlers        if process_batch_handlers?
  end

  def process_commit_handlers?
    commit_handlers.any? && new_commits.any?
  end

  def process_commit_range_handlers?
    commit_range_handlers.any? && new_commits.any?
  end

  def process_branch_handlers?
    branch_handlers.any? && send("process_#{branch.mode}_branch_handlers?")
  end

  def process_batch_handlers?
    batch_handlers.any? && new_commits.any?
  end

  def process_pr_branch_handlers?
    parent_branch_new_commits = statistics.fetch_path("master", :new_commits)
    new_commits.any? || parent_branch_new_commits.any?
  end

  def process_regular_branch_handlers?
    new_commits.any?
  end

  def process_commit_handlers
    new_commits_details.each do |commit, details|
      commit_handlers.each do |h|
        logger.info("Queueing #{h.name.split("::").last} for commit #{commit} on branch #{branch.name}")
        h.perform_async(branch.id, commit, details)
      end
    end
  end

  def process_commit_range_handlers
    commit_range = [new_commits.first, new_commits.last].uniq.join("..")

    commit_range_handlers.each do |h|
      logger.info("Queueing #{h.name.split("::").last} for commit range #{commit_range} on branch #{branch.name}")
      h.perform_async(branch.id, new_commits)
    end
  end

  def process_branch_handlers
    branch_handlers.each do |h|
      logger.info("Queueing #{h.name.split("::").last} for branch #{branch.name}")
      h.perform_async(branch.id)
    end
  end

  def process_batch_handlers
    batch_handlers.each do |h|
      logger.info("Queueing #{h.name} for branch #{branch.name}")
      h.perform_batch_async(branch.id, new_commits_details)
    end
  end
end