Skip to content

Instantly share code, notes, and snippets.

@Earlopain
Last active August 19, 2025 13:58
Show Gist options
  • Select an option

  • Save Earlopain/b307c9d8b243023c90045adde75ef87a to your computer and use it in GitHub Desktop.

Select an option

Save Earlopain/b307c9d8b243023c90045adde75ef87a to your computer and use it in GitHub Desktop.
Run each commit of a repo against current rubocop to check for cop errors
# frozen_string_literal: true
require "bundler/inline"
$extensions = {
"rubocop-rails" => "2.33.3",
"rubocop-rspec" => "3.6.0",
"rubocop-rspec_rails" => "2.31.0",
"rubocop-minitest" => "0.38.1",
"rubocop-performance" => "1.25.0",
"rubocop-capybara" => "2.22.1",
"rubocop-factory_bot" => "2.27.1",
"rubocop-rake" => "0.7.1",
"rubocop-thread_safety" => "0.7.3",
"rubocop-erb" => "0.6.0",
}
gemfile do
source "https://rubygems.org"
gem "vernier"
$extensions.each { |name, version| gem name, version }
end
profiling = false
Signal.trap("SIGUSR1") do
if profiling
Vernier.stop_profile
else
Vernier.start_profile(out: "#{__dir__}/time_profile.json")
end
profiling = !profiling
end
puts "PID: #{Process.pid}"
require "tempfile"
RubyVM::YJIT.enable
# Supress whatever warnings may occur (wrong department, regexp, etc.)
def Warning.warn(...)
end
# `rubocop:disable Rspec/PredicateMatcher` raises
class RuboCop::CommentConfig
def cop_disabled_line_ranges
{}
end
end
class DiffFormatter < RuboCop::Formatter::BaseFormatter
BASELINE_FILE = "baseline.json"
COMPARE_FILE = "compare.json"
DO_COMPARE = File.exist?(BASELINE_FILE)
OFFENSES = []
def self.finish
if DO_COMPARE
baseline_offenses = JSON.parse(File.read("baseline.json"))
new_offenses = OFFENSES - baseline_offenses
missing_offenses = baseline_offenses - OFFENSES
data = { new: new_offenses, missing: missing_offenses }
File.write(COMPARE_FILE, JSON.pretty_generate(data))
else
File.write(BASELINE_FILE, JSON.pretty_generate(OFFENSES))
end
end
def file_finished(file, offenses)
parts = offenses.filter_map do |offense|
next if offense.cop_name == "Lint/Syntax"
puts "#{file}:#{offense.line}:#{offense.real_column} - #{offense.message}"
"#{file}:#{offense.line}:#{offense.real_column} - #{offense.message}"
end
OFFENSES.concat(parts)
end
end
class Runner
attr_reader :target_ruby, :target_rails
def self.config_for_pwd
config_path = RuboCop::ConfigLoader.configuration_file_for("./")
begin
RuboCop::ConfigLoader.load_yaml_configuration(config_path)
rescue Psych::SyntaxError
{}
end
end
def initialize(only_cop: nil, formatter:)
@target_ruby, @target_rails = targets
@only_cop = only_cop
@formatter = formatter
@runner = create_runner
end
def run
@runner.run(["./"])
end
def errors
@runner.errors
end
def create_runner
opts = ["--stderr"]
opts += ["--format", @formatter]
opts += ["--only", @only_cop] if @only_cop
options = RuboCop::Options.new.parse(opts).first
config_store = RuboCop::ConfigStore.new
with_config do |config|
config_store.options_config = config.path
end
r = RuboCop::Runner.new(options, config_store)
def r.run(...)
super
raise Interrupt if aborting?
end
r
end
def targets
other_yml = self.class.config_for_pwd
other_all_cops = (other_yml.dig("AllCops") || {}).slice("TargetRubyVersion", "TargetRailsVersion")
other_config = RuboCop::Config.create({ "AllCops" => other_all_cops }, ".rubocop.yml", check: false)
target_ruby = RuboCop::TargetRuby.new(other_config).version
target_rails = other_config.target_rails_version
[target_ruby, target_rails]
end
def with_config
Tempfile.create do |f|
f << <<~YML
require: #{$extensions.keys.inspect}
AllCops:
EnabledByDefault: true
TargetRubyVersion: #{target_ruby}
TargetRailsVersion: #{target_rails}
MaxFilesInCache: 100000
Style/Copyright:
Enabled: false
#{without}
YML
f.rewind
yield(f)
end
end
def without
ENV.fetch("WITHOUT", "").split(",").map do |cop|
<<~YML
#{cop}:
Enabled: false
YML
end.join("\n")
end
end
class EachCommit
def initialize(folder, start)
@folder = folder
@start = start || `git branch`.split("\n")[1].strip
end
def run
Dir.chdir(@folder) do
system("git checkout #{@start}", exception: true)
remaining_commits = `git rev-list HEAD --count`.to_i
loop do
current_commit = `git rev-parse HEAD`.strip
raise StandardError, "rev-parse failed" if current_commit == ""
runner = Runner.new(formatter: "RuboCop::Formatter::BaseFormatter")
puts "Commit #{current_commit} (#{remaining_commits}), #{runner.target_ruby}/#{runner.target_rails}"
runner.run
if runner.errors.any?
puts "-" * 20
pp runner.errors
break
end
previous_commit = `git log --pretty=format:"%H" -2`.split("\n").last
break if previous_commit == current_commit
remaining_commits -= 1
system("git checkout #{previous_commit}", exception: true, out: File::NULL, err: File::NULL)
end
end
end
end
class SingleCop
def initialize(folder, cop_name)
@folder = folder
@cop_name = cop_name
end
def run
Dir.glob("#{@folder}/*").each do |path|
Dir.chdir(path) do
runner = Runner.new(only_cop: @cop_name, formatter: "RuboCop::Formatter::BaseFormatter")
puts "Folder #{path}, #{runner.target_ruby}/#{runner.target_rails}"
runner.run
if runner.errors.any?
puts "-" * 20
pp runner.errors
end
end
end
DiffFormatter.finish
end
end
EachCommit.new(ENV["FOLDER"], ENV["START"]).run
puts "DONE!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment