-
-
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 |
๐ Thanks!
๐ 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.
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)