-
-
Save michaelkl/56bffbe4e2fb282b6ef70a180b28187f to your computer and use it in GitHub Desktop.
A Rubocop wrapper that checks only added/modified code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# frozen_string_literal: true | |
# Source: https://gist.github.com/skanev/9d4bec97d5a6825eaaf6 | |
# | |
# A sneaky wrapper around Rubocop that allows you to run it only against | |
# the recent changes, as opposed to the whole project. It lets you | |
# enforce the style guide for new/modified code only, as opposed to | |
# having to restyle everything or adding cops incrementally. It relies | |
# on git to figure out which files to check. | |
# | |
# Here are some options you can pass in addition to the ones in rubocop: | |
# | |
# --local Check only the changes you are about to push | |
# to the remote repository. | |
# | |
# --uncommitted Check only changes in files that have not been | |
# --index committed (i.e. either in working directory or | |
# staged). | |
# | |
# --against REFSPEC Check changes since REFSPEC. This can be | |
# anything that git will recognize. | |
# | |
# --branch BASE_BRANCH Check only changes in the current branch | |
# compared to the given BASE_BRANCH or `master` | |
# by default. | |
# | |
# --courage Without this option, only the modified lines | |
# are inspected. When supplied, it will check | |
# the full contents of the file. You should have | |
# the courage to fix your style violations as | |
# you see them, you know. | |
# | |
# Caveat emptor: | |
# | |
# * Monkey patching ahead. This script relies on Rubocop internals and | |
# has been tested against 0.25.0. Newer (or older) versions might | |
# break it. | |
# | |
# * While it does try to check modified lines only, there might be some | |
# quirks. It might not show offenses in modified code if they are | |
# reported at unmodified lines. It might also show offenses in | |
# unmodified code if they are reported in modified lines. | |
require 'rubocop' | |
module DirtyCop | |
extend self # In your face, style guide! | |
def bury_evidence?(file, line) | |
!report_offense_at?(file, line) | |
end | |
def uncovered_targets | |
@files | |
end | |
def cover_up_unmodified(ref, only_changed_lines = true) | |
@files = files_modified_since(ref) | |
@line_filter = build_line_filter(@files, ref) if only_changed_lines | |
end | |
# rubocop:disable Metrics/CyclomaticComplexity | |
def process_bribe | |
eat_a_donut if ARGV.empty? | |
ref = nil | |
only_changed_lines = true | |
loop do | |
arg = ARGV.shift | |
case arg | |
when '--local' | |
ref = `git rev-parse --abbrev-ref --symbolic-full-name @{u}`.chomp | |
exit 1 unless $CHILD_STATUS.success? | |
when '--against' | |
ref = ARGV.shift | |
when '--uncommitted', '--index' | |
ref = 'HEAD' | |
when '--branch' | |
branch = ARGV.shift | |
ref = `git merge-base HEAD #{branch || 'master'}`.chomp | |
when '--courage' | |
only_changed_lines = false | |
else | |
ARGV.unshift arg | |
break | |
end | |
end | |
return unless ref | |
cover_up_unmodified ref, only_changed_lines | |
end | |
# rubocop:enable Metrics/CyclomaticComplexity | |
private | |
def report_offense_at?(file, line) | |
!@line_filter || @line_filter.fetch(file)[line] | |
end | |
def files_modified_since(ref) | |
`git diff --diff-filter=AM --name-only #{ref}`. | |
lines. | |
map(&:chomp). | |
grep(/\.rb$/). | |
map { |file| File.absolute_path(file) } | |
end | |
def build_line_filter(_files, ref) | |
result = {} | |
suspects = files_modified_since(ref) | |
suspects.each do |file| | |
result[file] = lines_modified_since(file, ref) | |
end | |
result | |
end | |
def lines_modified_since(file, ref) | |
ranges = | |
`git diff -p -U0 #{ref} #{file}`. | |
lines. | |
grep(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/) { $1.to_i..($1.to_i + $2.to_i) }. # rubocop:disable Style/PerlBackrefs | |
reverse | |
mask = Array.new(ranges.first.end) | |
ranges.each do |range| | |
range.each do |line| | |
mask[line] = true | |
end | |
end | |
mask | |
end | |
def eat_a_donut | |
puts "#{$PROGRAM_NAME}: The dirty cop Alex Murphy could have been" | |
puts | |
puts File.read(__FILE__)[/(?:^#(?:[^!].*)?\n)+/s].gsub(/^#/, ' ') | |
exit | |
end | |
end | |
module RuboCop | |
class TargetFinder | |
alias find_unpatched find | |
def find(args) | |
targets = find_unpatched(args) | |
replacements = DirtyCop.uncovered_targets | |
return targets unless replacements | |
replacements & targets | |
end | |
end | |
class Runner | |
alias file_offenses_unpatched file_offenses | |
def file_offenses(file) | |
file_offenses_unpatched(file). | |
reject { |o| DirtyCop.bury_evidence?(o.location.source_buffer.name, o.location.line) } | |
end | |
end | |
end | |
DirtyCop.process_bribe | |
exit RuboCop::CLI.new.run |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Made some fixes, tested on Rubocop 0.74.0.
Made it respect exclusions defined in the config.