Last active
March 13, 2024 08:24
-
-
Save skanev/9d4bec97d5a6825eaaf6 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 | |
# 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 Check only changes in the current branch. | |
# | |
# --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 | |
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 $?.success? | |
when '--against' | |
ref = ARGV.shift | |
when '--uncommitted', '--index' | |
ref = 'HEAD' | |
when '--branch' | |
ref = `git merge-base HEAD 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 | |
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) }. | |
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) | |
replacement = DirtyCop.uncovered_targets | |
return replacement if replacement | |
find_unpatched(args) | |
end | |
end | |
class Runner | |
alias inspect_file_unpatched inspect_file | |
def inspect_file(file) | |
offenses, updated = inspect_file_unpatched(file) | |
offenses = offenses.reject { |o| DirtyCop.bury_evidence?(file.path, o.line) } | |
[offenses, updated] | |
end | |
end | |
end | |
DirtyCop.process_bribe | |
exit RuboCop::CLI.new.run |
Another issue is that --local won't work if you're pushing the branch for the first time (i.e. there is no tracking branch). You'd need to catch that error (git would return "fatal: no upstream configured for branch 'some-branch'") and replace it. My Git-fu is a bit lacking so not 100% sure how to solve this, but you'd need to check the most recent commit which has an equivalent commit on origin.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@pelargir's fix only shift the bug. With it, violations on the lines exactly after changes of more than 1 line will also be returned.
A correct fix is to change:
$1.to_i...($1.to_i + $2.to_i)
to:
$1.to_i...($1.to_i + ($2 || 1).to_i)
I made a fork of the gist which this fix, a few other fixes/improvements. (can use --staged to check only the code that is currently staged, not limited to only .rb files)
https://gist.github.com/MaxLap/ea4b6d1df81de3024562798b5501b9c8