Skip to content

Instantly share code, notes, and snippets.

@skanev
Last active March 13, 2024 08:24
Show Gist options
  • Star 66 You must be signed in to star a gist
  • Fork 21 You must be signed in to fork a gist
  • Save skanev/9d4bec97d5a6825eaaf6 to your computer and use it in GitHub Desktop.
Save skanev/9d4bec97d5a6825eaaf6 to your computer and use it in GitHub Desktop.
A Rubocop wrapper that checks only added/modified code
#!/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
@pelargir
Copy link

The script above has a minor bug: the range should be inclusive instead of exclusive. As it sits now, the script won't recognize a violation if it's a single line in the file. To fix change the range on line 118 from 3 dots to 2 dots. For example:

$1.to_i...($1.to_i + $2.to_i)

becomes

$1.to_i..($1.to_i + $2.to_i)

@hrishikesh
Copy link

👍 Thanks!

@matrinox
Copy link

👍 Big help! Beats pushing to github just to check the next 10 styles

@MaxLap
Copy link

MaxLap commented Nov 15, 2016

@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

@dorner
Copy link

dorner commented Jun 23, 2017

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