Run rubocop with lefthook only on modified lines

Inspired and adapted from
  https://gist.github.com/skanev/9d4bec97d5a6825eaaf6
  https://gist.github.com/MaxLap/ea4b6d1df81de3024562798b5501b9c8
pull/10894/head
Christophe Bliard 2 years ago
parent e25d2f14ee
commit d1ce52f9f0
No known key found for this signature in database
GPG Key ID: 2BC07603210C3FA4
  1. 202
      bin/dirty-rubocop
  2. 2
      lefthook.yml

@ -0,0 +1,202 @@
#!/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.
#
# --staged Check only changes that are currently staged.
#
# --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.
#
# Caveat emptor:
#
# * Monkey patching ahead. This script relies on Rubocop internals and
# has been tested against 1.30.1. 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.
# Inspired and adapted from
# https://gist.github.com/skanev/9d4bec97d5a6825eaaf6
# https://gist.github.com/MaxLap/ea4b6d1df81de3024562798b5501b9c8
require 'rubocop'
module DirtyCop
extend self # In your face, style guide!
def bury_evidence?(file, line)
!report_offense_at?(file, line)
end
def bury_evidences(file, offenses)
offenses.reject { |o| bury_evidence?(file, o.line) }
end
def staged_changes_only?
!!@staged_changes_only
end
def uncovered_targets
@files
end
def cover_up_unmodified(ref)
@files = files_modified_since(ref)
@line_filter = build_line_filter(@files, ref)
end
def process_bribe
eat_a_donut if ARGV.empty?
ref = nil
loop do
arg = ARGV.shift
case arg
when '--local'
ref = `git rev-parse --abbrev-ref --symbolic-full-name @{u}`
exit 1 unless $?.success?
when '--staged'
ref = '--cached'
@staged_changes_only = true
ARGV << "--cache=false"
when '--against'
ref = ARGV.shift
when '--uncommitted', '--index'
ref = 'HEAD'
when '--branch'
ref = `git merge-base HEAD dev`
else
ARGV.unshift arg
break
end
end
return unless ref
cover_up_unmodified ref.chomp
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)
.map { |file| File.absolute_path(file) }
end
def build_line_filter(suspects, ref)
result = {}
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 || 1).to_i) }
.reverse
return [] if ranges.empty?
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(...)
every_files = find_unpatched(...)
modified_files = DirtyCop.uncovered_targets
if modified_files
every_files & modified_files
else
every_files
end
end
end
class Runner
alias inspect_file_unpatched inspect_file
def inspect_file(file, *args)
offenses, updated = inspect_file_unpatched(file, *args)
offenses = offenses.reject { |o| DirtyCop.bury_evidence?(file.path, o.line) }
[offenses, updated]
end
alias add_redundant_disables_unpatched add_redundant_disables
def add_redundant_disables(file, offenses, source)
offenses = add_redundant_disables_unpatched(file, offenses, source)
DirtyCop.bury_evidences(file, offenses)
end
end
class ProcessedSource
class << self
alias from_file_unpatched from_file
end
def self.from_file(path, ruby_version)
if DirtyCop.staged_changes_only?
pathname = Pathname.new(path)
git_root = Pathname.new(`git rev-parse --show-toplevel`.strip)
git_relative_path = pathname.relative_path_from(git_root).to_s
source = `git show :#{git_relative_path}`
new(source, ruby_version, path)
else
from_file_unpatched(path, ruby_version)
end
end
end
end
DirtyCop.process_bribe
exit RuboCop::CLI.new.run

@ -16,4 +16,4 @@ pre-commit:
rubocop: rubocop:
files: git diff --name-only --staged files: git diff --name-only --staged
glob: "*.rb" glob: "*.rb"
run: bundle exec rubocop --force-exclusion {files} run: bin/dirty-rubocop --uncommitted --force-exclusion {files}

Loading…
Cancel
Save