-
-
Save skanev/9d4bec97d5a6825eaaf6 to your computer and use it in GitHub Desktop.
#!/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 |
👍 Big help! Beats pushing to github just to check the next 10 styles
@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
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.
👍 Thanks!