├── Gemfile ├── test ├── test_helper.rb └── rubocop │ └── git │ ├── options_test.rb │ ├── runner_test.rb │ └── cli_test.rb ├── lib └── rubocop │ ├── git │ ├── version.rb │ ├── file_violation.rb │ ├── line.rb │ ├── pseudo_resource.rb │ ├── commit.rb │ ├── diff_parser.rb │ ├── pseudo_pull_request.rb │ ├── patch.rb │ ├── commit_file.rb │ ├── style_checker.rb │ ├── runner.rb │ ├── cli.rb │ ├── options.rb │ └── style_guide.rb │ └── git.rb ├── bin └── rubocop-git ├── gemfiles ├── 0.24.gemfile ├── 0.25.gemfile ├── 0.26.gemfile ├── 0.27.gemfile ├── 0.28.gemfile ├── 0.29.gemfile ├── 0.30.gemfile ├── 0.31.gemfile ├── 0.32.gemfile ├── 0.33.gemfile ├── 0.34.gemfile ├── 0.35.gemfile ├── 0.36.gemfile ├── 0.37.gemfile ├── 0.38.gemfile ├── 0.39.gemfile ├── 0.40.gemfile ├── 0.41.gemfile ├── 0.42.gemfile ├── 0.43.gemfile ├── 0.44.gemfile ├── 0.45.gemfile ├── 0.46.gemfile ├── 0.47.gemfile └── 0.48.gemfile ├── .gitignore ├── Rakefile ├── Appraisals ├── .travis.yml ├── rubocop-git.gemspec ├── LICENSE.txt ├── README.md └── hound.yml /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'rubocop/git' 3 | -------------------------------------------------------------------------------- /lib/rubocop/git/version.rb: -------------------------------------------------------------------------------- 1 | module RuboCop 2 | module Git 3 | VERSION = '0.1.3' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /bin/rubocop-git: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubocop/git/cli' 4 | 5 | RuboCop::Git::CLI.new.run 6 | -------------------------------------------------------------------------------- /gemfiles/0.24.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.24.1" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.25.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.25.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.26.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.26.1" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.27.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.27.1" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.28.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.28.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.29.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.29.1" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.30.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.30.1" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.31.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.31.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.32.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.32.1" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.33.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.33.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.34.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.34.2" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.35.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.35.1" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.36.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.36.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.37.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.37.2" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.38.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.38.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.39.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.39.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.40.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.40.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.41.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.41.2" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.42.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.42.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.43.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.43.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.44.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.44.1" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.45.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.45.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.46.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.46.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.47.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.47.1" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/0.48.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rubocop", "0.48.1" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | /gemfiles/*.gemfile.lock 12 | -------------------------------------------------------------------------------- /lib/rubocop/git/file_violation.rb: -------------------------------------------------------------------------------- 1 | module RuboCop::Git 2 | # ref. https://github.com/thoughtbot/hound/blob/d2f3933/app/models/file_violation.rb 3 | class FileViolation < Struct.new(:filename, :offenses) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/rubocop/git/line.rb: -------------------------------------------------------------------------------- 1 | module RuboCop::Git 2 | # copy from https://github.com/thoughtbot/hound/blob/d2f3933/app/models/line.rb 3 | class Line < Struct.new(:content, :line_number, :patch_position) 4 | def ==(other_line) 5 | content == other_line.content 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | require 'appraisal' 4 | 5 | Rake::TestTask.new(:test) do |t| 6 | t.libs << 'test' 7 | t.libs << 'lib' 8 | t.test_files = FileList['test/**/*_test.rb'] 9 | end 10 | 11 | if ENV['APPRAISAL_INITIALIZED'] || ENV['TRAVIS'] 12 | task default: :test 13 | else 14 | task default: :appraisal 15 | end 16 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | %w( 2 | 0.24.1 3 | 0.25.0 4 | 0.26.1 5 | 0.27.1 6 | 0.28.0 7 | 0.29.1 8 | 0.30.1 9 | 0.31.0 10 | 0.32.1 11 | 0.33.0 12 | 0.34.2 13 | 0.35.1 14 | 0.36.0 15 | 0.37.2 16 | 0.38.0 17 | 0.39.0 18 | 0.40.0 19 | 0.41.2 20 | 0.42.0 21 | 0.43.0 22 | 0.44.1 23 | 0.45.0 24 | 0.46.0 25 | 0.47.1 26 | 0.48.1 27 | ).each do |version| 28 | appraise version[/\d+\.\d+/] do 29 | gem 'rubocop', version 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/rubocop/git/options_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | require 'rubocop/git/options' 3 | 4 | describe RuboCop::Git::Options do 5 | it 'fail with no options' do 6 | proc do 7 | RuboCop::Git::Options.new({}) 8 | end.must_raise(RuboCop::Git::Options::Invalid) 9 | end 10 | 11 | it 'can pass string hash options' do 12 | RuboCop::Git::Options.new('rubocop' => {}, 'commits' => []) 13 | end 14 | 15 | it 'can pass symbol hash options' do 16 | RuboCop::Git::Options.new(rubocop: {}, commits: []) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rubocop/git/pseudo_resource.rb: -------------------------------------------------------------------------------- 1 | module RuboCop 2 | module Git 3 | class PseudoResource 4 | attr_reader :patch, :pwd, :file_relative_path 5 | 6 | alias_method :filename, :file_relative_path 7 | 8 | def initialize(file_relative_path, pwd = Dir.pwd) 9 | @file_relative_path = file_relative_path 10 | @pwd = pwd 11 | @patch = '' 12 | end 13 | 14 | def absolute_path 15 | filename 16 | File.join(pwd, filename) 17 | end 18 | 19 | def status 20 | 'modified' 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/rubocop/git/runner_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | require 'rubocop/git/runner' 3 | 4 | describe RuboCop::Git::Runner do 5 | it 'returns false' do 6 | options = RuboCop::Git::Options.new 7 | # lib/rubocop/git/runner.rb:14:1: C: Trailing whitespace detected. 8 | options.commits = ["v0.0.4", "v0.0.5"] 9 | refute(RuboCop::Git::Runner.new.run(options)) 10 | end 11 | 12 | it 'fail with no options' do 13 | proc do 14 | _out, _err = capture_io do 15 | RuboCop::Git::Runner.new.run({}) 16 | end 17 | end.must_raise(RuboCop::Git::Options::Invalid) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/rubocop/git/commit.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | module RuboCop 4 | module Git 5 | # ref. https://github.com/thoughtbot/hound/blob/d2f3933/app/models/commit.rb 6 | class Commit 7 | def initialize(options) 8 | @options = options 9 | end 10 | 11 | def file_content(filename) 12 | if @options.cached 13 | `git show :#{filename.shellescape}` 14 | elsif @options.commit_last 15 | `git show #{@options.commit_last.shellescape}:#{filename.shellescape}` 16 | else 17 | File.read(filename) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/rubocop/git/cli_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | require 'rubocop/git/cli' 3 | 4 | describe RuboCop::Git::CLI do 5 | it 'exit with violations' do 6 | # lib/rubocop/git/runner.rb:14:1: C: Trailing whitespace detected. 7 | commits = ["v0.0.4", "v0.0.5"] 8 | proc do 9 | _out, _err = capture_io do 10 | RuboCop::Git::CLI.new.run(commits) 11 | end 12 | end.must_raise(SystemExit) 13 | end 14 | 15 | it 'fail with invalid options' do 16 | proc do 17 | _out, _err = capture_io do 18 | RuboCop::Git::CLI.new.run(['--gruß']) 19 | end 20 | end.must_raise(SystemExit) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/rubocop/git/diff_parser.rb: -------------------------------------------------------------------------------- 1 | module RuboCop 2 | module Git 3 | class DiffParser 4 | class << self 5 | def parse(diff) 6 | new.parse(diff) 7 | end 8 | end 9 | 10 | def parse(diff) 11 | files = [] 12 | in_patch = false 13 | 14 | diff.each_line do |line| 15 | case line 16 | when /^diff --git/ 17 | in_patch = false 18 | when %r{^\+{3} b/(?[^\t\n\r]+)} 19 | files << PseudoResource.new(Regexp.last_match[:path]) 20 | when /^@@/ 21 | in_patch = true 22 | end 23 | 24 | files.last.patch << line if in_patch 25 | end 26 | 27 | files 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/rubocop/git.rb: -------------------------------------------------------------------------------- 1 | require 'rubocop/git/version' 2 | require 'rubocop' 3 | 4 | module RuboCop 5 | module Git 6 | autoload :Commit, 'rubocop/git/commit' 7 | autoload :CommitFile, 'rubocop/git/commit_file' 8 | autoload :DiffParser, 'rubocop/git/diff_parser' 9 | autoload :FileViolation, 'rubocop/git/file_violation' 10 | autoload :Line, 'rubocop/git/line' 11 | autoload :Options, 'rubocop/git/options' 12 | autoload :Patch, 'rubocop/git/patch' 13 | autoload :PseudoPullRequest, 'rubocop/git/pseudo_pull_request' 14 | autoload :PseudoResource, 'rubocop/git/pseudo_resource' 15 | autoload :Runner, 'rubocop/git/runner' 16 | autoload :StyleChecker, 'rubocop/git/style_checker' 17 | autoload :StyleGuide, 'rubocop/git/style_guide' 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1.10 4 | - 2.4.1 5 | gemfile: 6 | - gemfiles/0.24.gemfile 7 | - gemfiles/0.25.gemfile 8 | - gemfiles/0.26.gemfile 9 | - gemfiles/0.27.gemfile 10 | - gemfiles/0.28.gemfile 11 | - gemfiles/0.29.gemfile 12 | - gemfiles/0.30.gemfile 13 | - gemfiles/0.31.gemfile 14 | - gemfiles/0.32.gemfile 15 | - gemfiles/0.33.gemfile 16 | - gemfiles/0.34.gemfile 17 | - gemfiles/0.35.gemfile 18 | - gemfiles/0.36.gemfile 19 | - gemfiles/0.37.gemfile 20 | - gemfiles/0.38.gemfile 21 | - gemfiles/0.39.gemfile 22 | - gemfiles/0.40.gemfile 23 | - gemfiles/0.41.gemfile 24 | - gemfiles/0.42.gemfile 25 | - gemfiles/0.43.gemfile 26 | - gemfiles/0.44.gemfile 27 | - gemfiles/0.45.gemfile 28 | - gemfiles/0.46.gemfile 29 | - gemfiles/0.47.gemfile 30 | - gemfiles/0.48.gemfile 31 | before_install: 32 | - gem update --system 33 | -------------------------------------------------------------------------------- /lib/rubocop/git/pseudo_pull_request.rb: -------------------------------------------------------------------------------- 1 | module RuboCop 2 | module Git 3 | # ref. https://github.com/thoughtbot/hound/blob/d2f3933/app/models/pull_request.rb 4 | class PseudoPullRequest 5 | HOUND_CONFIG_FILE = '.hound.yml' 6 | 7 | def initialize(files, options) 8 | @files = files 9 | @options = options 10 | end 11 | 12 | def pull_request_files 13 | @files.map do |file| 14 | build_commit_file(file) 15 | end 16 | end 17 | 18 | def config 19 | return unless @options.hound 20 | File.read(HOUND_CONFIG_FILE) 21 | rescue Errno::ENOENT 22 | nil 23 | end 24 | 25 | private 26 | 27 | def build_commit_file(file) 28 | CommitFile.new(file, head_commit) 29 | end 30 | 31 | def head_commit 32 | @head_commit ||= Commit.new(@options) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/rubocop/git/patch.rb: -------------------------------------------------------------------------------- 1 | module RuboCop::Git 2 | # copy from https://github.com/thoughtbot/hound/blob/d2f3933/app/models/patch.rb 3 | class Patch 4 | RANGE_INFORMATION_LINE = /^@@ .+\+(?\d+),/ 5 | MODIFIED_LINE = /^\+(?!\+|\+)/ 6 | NOT_REMOVED_LINE = /^[^-]/ 7 | 8 | def initialize(body) 9 | @body = body || '' 10 | end 11 | 12 | def additions 13 | line_number = 0 14 | 15 | lines.each_with_index.inject([]) do |additions, (content, patch_position)| 16 | case content 17 | when RANGE_INFORMATION_LINE 18 | line_number = Regexp.last_match[:line_number].to_i 19 | when MODIFIED_LINE 20 | additions << Line.new(content, line_number, patch_position) 21 | line_number += 1 22 | when NOT_REMOVED_LINE 23 | line_number += 1 24 | end 25 | 26 | additions 27 | end 28 | end 29 | 30 | private 31 | 32 | def lines 33 | @body.lines 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /rubocop-git.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'rubocop/git/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'rubocop-git' 8 | spec.version = RuboCop::Git::VERSION 9 | spec.authors = ['Masaki Takeuchi'] 10 | spec.email = ['m.ishihara@gmail.com'] 11 | spec.summary = %q{RuboCop for git diff.} 12 | spec.description = %q{RuboCop for git diff.} 13 | spec.homepage = 'https://github.com/m4i/rubocop-git' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '~> 1.6' 22 | spec.add_development_dependency 'rake' 23 | spec.add_development_dependency 'minitest' 24 | spec.add_development_dependency 'appraisal' 25 | 26 | spec.add_dependency 'rubocop', '>= 0.24.1' 27 | end 28 | -------------------------------------------------------------------------------- /lib/rubocop/git/commit_file.rb: -------------------------------------------------------------------------------- 1 | module RuboCop::Git 2 | # copy from https://github.com/thoughtbot/hound/blob/d2f3933/app/models/commit_file.rb 3 | class CommitFile 4 | def initialize(file, commit) 5 | @file = file 6 | @commit = commit 7 | end 8 | 9 | def absolute_path 10 | @file.absolute_path 11 | end 12 | 13 | def filename 14 | @file.filename 15 | end 16 | 17 | def content 18 | @content ||= begin 19 | unless removed? 20 | @commit.file_content(filename) 21 | end 22 | end 23 | end 24 | 25 | def relevant_line?(line_number) 26 | modified_lines.detect do |modified_line| 27 | modified_line.line_number == line_number 28 | end 29 | end 30 | 31 | def removed? 32 | @file.status == 'removed' 33 | end 34 | 35 | def ruby? 36 | filename.match(/.*\.rb$/) 37 | end 38 | 39 | def modified_lines 40 | @modified_lines ||= patch.additions 41 | end 42 | 43 | def modified_line_at(line_number) 44 | modified_lines.detect do |modified_line| 45 | modified_line.line_number == line_number 46 | end 47 | end 48 | 49 | private 50 | 51 | def patch 52 | Patch.new(@file.patch) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Masaki Takeuchi 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | Some code is: 25 | Copyright (c) 2014 thoughtbot, inc. 26 | Released under the MIT License in Hound, https://github.com/thoughtbot/hound 27 | -------------------------------------------------------------------------------- /lib/rubocop/git/style_checker.rb: -------------------------------------------------------------------------------- 1 | module RuboCop::Git 2 | # ref. https://github.com/thoughtbot/hound/blob/d2f3933/app/models/style_checker.rb 3 | class StyleChecker 4 | def initialize(modified_files, 5 | rubocop_options, 6 | config_file, 7 | custom_config = nil) 8 | @modified_files = modified_files 9 | @rubocop_options = rubocop_options 10 | @config_file = config_file 11 | @custom_config = custom_config 12 | end 13 | 14 | def violations 15 | file_violations = @modified_files.map do |modified_file| 16 | FileViolation.new(modified_file.absolute_path, offenses(modified_file)) 17 | end 18 | 19 | file_violations.select do |file_violation| 20 | file_violation.offenses.any? 21 | end 22 | end 23 | 24 | private 25 | 26 | def offenses(modified_file) 27 | violations = style_guide.violations(modified_file) 28 | violations_on_changed_lines(modified_file, violations) 29 | end 30 | 31 | def violations_on_changed_lines(modified_file, violations) 32 | violations.select do |violation| 33 | modified_file.relevant_line?(violation.line) 34 | end 35 | end 36 | 37 | def style_guide 38 | @style_guide ||= StyleGuide.new(@rubocop_options, 39 | @config_file, 40 | @custom_config) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RuboCop::Git 2 | 3 | RuboCop for git diff. 4 | 5 | [![Gem Version](https://badge.fury.io/rb/rubocop-git.svg)](http://badge.fury.io/rb/rubocop-git) 6 | [![Build Status](https://travis-ci.org/m4i/rubocop-git.svg?branch=master)](https://travis-ci.org/m4i/rubocop-git) 7 | [![Code Climate](https://codeclimate.com/github/m4i/rubocop-git.png)](https://codeclimate.com/github/m4i/rubocop-git) 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | gem 'rubocop-git' 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install rubocop-git 22 | 23 | ## Usage 24 | 25 | Usage: rubocop-git [options] [[commit] commit] 26 | -c, --config FILE Specify configuration file 27 | -r, --require FILE Require Ruby file 28 | -d, --debug Display debug info 29 | -D, --display-cop-names Display cop names in offense messages 30 | --cached git diff --cached 31 | --staged synonym of --cached 32 | --hound Hound compatibility mode 33 | 34 | ## Contributing 35 | 36 | 1. Fork it ( https://github.com/m4i/rubocop-git/fork ) 37 | 2. Create your feature branch (`git checkout -b my-new-feature`) 38 | 3. Commit your changes (`git commit -am 'Add some feature'`) 39 | 4. Push to the branch (`git push origin my-new-feature`) 40 | 5. Create a new Pull Request 41 | -------------------------------------------------------------------------------- /lib/rubocop/git/runner.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | module RuboCop 4 | module Git 5 | # ref. https://github.com/thoughtbot/hound/blob/d2f3933/app/services/build_runner.rb 6 | class Runner 7 | def run(options) 8 | options = Options.new(options) unless options.is_a?(Options) 9 | 10 | @options = options 11 | @files = DiffParser.parse(git_diff(options)) 12 | 13 | display_violations($stdout) 14 | 15 | violations.empty? 16 | end 17 | 18 | private 19 | 20 | def violations 21 | @violations ||= style_checker.violations 22 | end 23 | 24 | def style_checker 25 | StyleChecker.new(pull_request.pull_request_files, 26 | @options.rubocop, 27 | @options.config_file, 28 | pull_request.config) 29 | end 30 | 31 | def pull_request 32 | @pull_request ||= PseudoPullRequest.new(@files, @options) 33 | end 34 | 35 | def git_diff(options) 36 | args = %w(diff --diff-filter=AMCR --find-renames --find-copies) 37 | 38 | args << '--cached' if options.cached 39 | args << options.commit_first.shellescape if options.commit_first 40 | args << options.commit_last.shellescape if options.commit_last 41 | 42 | `git #{args.join(' ')}` 43 | end 44 | 45 | def display_violations(io) 46 | formatter = RuboCop::Formatter::ClangStyleFormatter.new(io) 47 | formatter.started(nil) 48 | 49 | violations.map do |violation| 50 | offenses = violation.offenses 51 | offenses = offenses.reject(&:disabled?) if offenses.first.respond_to?(:disabled?) 52 | formatter.file_finished( 53 | violation.filename, 54 | offenses.compact.sort.freeze 55 | ) 56 | end 57 | 58 | formatter.finished(@files.map(&:filename).freeze) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/rubocop/git/cli.rb: -------------------------------------------------------------------------------- 1 | require 'rubocop/git' 2 | require 'optparse' 3 | 4 | module RuboCop 5 | module Git 6 | class CLI 7 | def run(args = ARGV) 8 | @options = Options.new 9 | parse_arguments(args) 10 | if Runner.new.run(@options) 11 | exit 12 | else 13 | exit 1 14 | end 15 | end 16 | 17 | private 18 | 19 | def parse_arguments(args) 20 | @options.commits = option_parser.parse(args) 21 | rescue OptionParser::InvalidOption, Options::Invalid => ex 22 | warn "ERROR: #{ex.message}" 23 | $stderr.puts 24 | warn option_parser 25 | exit 1 26 | end 27 | 28 | def option_parser 29 | @option_parser ||= OptionParser.new do |opt| 30 | opt.banner << ' [[commit] commit]' 31 | 32 | opt.on('-c', '--config FILE', 33 | 'Specify configuration file') do |config| 34 | @options.config = config 35 | end 36 | 37 | opt.on('-r', '--require FILE', 38 | 'Require Ruby file') do |file| 39 | require file 40 | end 41 | 42 | opt.on('-d', '--debug', 'Display debug info') do 43 | @options.rubocop[:debug] = true 44 | end 45 | 46 | opt.on('-D', '--display-cop-names', 47 | 'Display cop names in offense messages') do 48 | @options.rubocop[:display_cop_names] = true 49 | end 50 | 51 | opt.on('--cached', 'git diff --cached') do 52 | @options.cached = true 53 | end 54 | 55 | opt.on('--staged', 'synonym of --cached') do 56 | @options.cached = true 57 | end 58 | 59 | opt.on('--hound', 'Hound compatibility mode') do 60 | @options.hound = true 61 | end 62 | 63 | opt.on('--version', 'Display version') do 64 | puts RuboCop::Git::VERSION 65 | exit 0 66 | end 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/rubocop/git/options.rb: -------------------------------------------------------------------------------- 1 | module RuboCop 2 | module Git 3 | class Options 4 | class Invalid < StandardError; end 5 | 6 | HOUND_DEFAULT_CONFIG_FILE = 7 | File.expand_path('../../../../hound.yml', __FILE__) 8 | 9 | attr_accessor :config 10 | attr_reader :cached, :hound, :rubocop 11 | 12 | def initialize(hash_options = nil) 13 | @config = nil 14 | @cached = false 15 | @hound = false 16 | @rubocop = {} 17 | @commits = [] 18 | 19 | from_hash(hash_options) if hash_options 20 | end 21 | 22 | def cached=(cached_) 23 | if cached_ && !@commits.empty? 24 | fail Invalid, 'cached and commit cannot be specified together' 25 | end 26 | @cached = !!cached_ 27 | end 28 | 29 | def hound=(hound_) 30 | @hound = !!hound_ 31 | end 32 | 33 | def rubocop=(rubocop_) 34 | unless rubocop_.is_a?(Hash) 35 | fail Invalid, "invalid rubocop: #{rubocop_.inspect}" 36 | end 37 | @rubocop = rubocop_ 38 | end 39 | 40 | def commits=(commits) 41 | unless commits.is_a?(Array) && commits.length <= 2 42 | fail Invalid, "invalid commits: #{commits.inspect}" 43 | end 44 | if !commits.empty? && cached 45 | fail Invalid, 'cached and commit cannot be specified together' 46 | end 47 | @commits = commits 48 | end 49 | 50 | def config_file 51 | if hound 52 | HOUND_DEFAULT_CONFIG_FILE 53 | elsif config 54 | config 55 | elsif File.exist?(RuboCop::ConfigLoader::DOTFILE) 56 | RuboCop::ConfigLoader::DOTFILE 57 | else 58 | RuboCop::ConfigLoader::DEFAULT_FILE 59 | end 60 | end 61 | 62 | def commit_first 63 | @commits.first 64 | end 65 | 66 | def commit_last 67 | @commits.length == 1 ? false : @commits.last 68 | end 69 | 70 | private 71 | 72 | def from_hash(hash_options) 73 | hash_options = hash_options.dup 74 | %w(config cached hound rubocop commits).each do |key| 75 | value = hash_options.delete(key) || hash_options.delete(key.to_sym) 76 | public_send("#{key}=", value) 77 | end 78 | unless hash_options.empty? 79 | fail Invalid, "invalid keys: #{hash_options.keys.join(' ')}" 80 | end 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/rubocop/git/style_guide.rb: -------------------------------------------------------------------------------- 1 | module RuboCop::Git 2 | # ref. https://github.com/thoughtbot/hound/blob/d2f3933/app/models/style_guide.rb 3 | class StyleGuide 4 | def initialize(rubocop_options, config_file, override_config_content = nil) 5 | @rubocop_options = rubocop_options 6 | @config_file = config_file 7 | @override_config_content = override_config_content 8 | end 9 | 10 | def violations(file) 11 | if ignored_file?(file) 12 | [] 13 | else 14 | parsed_source = parse_source(file) 15 | team = RuboCop::Cop::Team.new(all_cops, config, rubocop_options) 16 | team.inspect_file(parsed_source) 17 | end 18 | end 19 | 20 | private 21 | 22 | if Gem::Version.new(RuboCop::Version::STRING) >= Gem::Version.new('0.47.0') 23 | def all_cops; RuboCop::Cop::Registry.new RuboCop::Cop::Cop.all; end 24 | else 25 | def all_cops; RuboCop::Cop::Cop.all; end 26 | end 27 | 28 | def ignored_file?(file) 29 | !file.ruby? || file.removed? || excluded_file?(file) 30 | end 31 | 32 | def excluded_file?(file) 33 | config.file_to_exclude?(file.absolute_path) 34 | end 35 | 36 | def parse_source(file) 37 | rubocop_version = Gem::Version.new(RuboCop::Version::STRING) 38 | if rubocop_version < Gem::Version.new('0.36.0') 39 | RuboCop::ProcessedSource.new(file.content, file.absolute_path) 40 | elsif rubocop_version < Gem::Version.new('0.41.0') 41 | RuboCop::ProcessedSource.new(file.content, 42 | target_ruby_version, file.absolute_path) 43 | else 44 | RuboCop::ProcessedSource.new(file.content, 45 | config.target_ruby_version, file.absolute_path) 46 | end 47 | end 48 | 49 | def config 50 | if @config.nil? 51 | config = RuboCop::ConfigLoader.configuration_from_file(@config_file) 52 | combined_config = RuboCop::ConfigLoader.merge(config, override_config) 53 | @config = RuboCop::Config.new(combined_config, "") 54 | end 55 | 56 | @config 57 | end 58 | 59 | def rubocop_options 60 | if config["ShowCopNames"] 61 | { debug: true } 62 | else 63 | {} 64 | end.merge(@rubocop_options) 65 | end 66 | 67 | def override_config 68 | if @override_config_content 69 | config_content = YAML.load(@override_config_content) 70 | override_config = RuboCop::Config.new(config_content, "") 71 | override_config.add_missing_namespaces 72 | override_config.make_excludes_absolute 73 | override_config 74 | else 75 | {} 76 | end 77 | end 78 | 79 | # TODO: DELETE ME when we drop support for 0.x releases of rubocop 80 | # 81 | # This method exists in RuboCop::Config now (or config in this class) so we 82 | # should make use of that. 83 | def target_ruby_version 84 | @target ||= begin 85 | target = config['AllCops'] && config['AllCops']['TargetRubyVersion'] 86 | 87 | if !target || !RuboCop::Config::KNOWN_RUBIES.include?(target) 88 | fail ValidationError, "Unknown Ruby version #{target.inspect} found " \ 89 | 'in `TargetRubyVersion` parameter (in ' \ 90 | "#{loaded_path}).\nKnown versions: " \ 91 | "#{RuboCop::Config::KNOWN_RUBIES.join(', ')}" 92 | end 93 | 94 | target 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /hound.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - db/schema.rb 4 | 5 | AccessorMethodName: 6 | Enabled: false 7 | 8 | ActionFilter: 9 | Enabled: false 10 | 11 | Alias: 12 | Enabled: false 13 | 14 | ArrayJoin: 15 | Enabled: false 16 | 17 | AsciiComments: 18 | Enabled: false 19 | 20 | AsciiIdentifiers: 21 | Enabled: false 22 | 23 | Attr: 24 | Enabled: false 25 | 26 | BlockNesting: 27 | Enabled: false 28 | 29 | CaseEquality: 30 | Enabled: false 31 | 32 | CharacterLiteral: 33 | Enabled: false 34 | 35 | ClassAndModuleChildren: 36 | Enabled: false 37 | 38 | ClassLength: 39 | Enabled: false 40 | 41 | ClassVars: 42 | Enabled: false 43 | 44 | CollectionMethods: 45 | PreferredMethods: 46 | find: detect 47 | reduce: inject 48 | collect: map 49 | find_all: select 50 | 51 | ColonMethodCall: 52 | Enabled: false 53 | 54 | CommentAnnotation: 55 | Enabled: false 56 | 57 | CyclomaticComplexity: 58 | Enabled: false 59 | 60 | Delegate: 61 | Enabled: false 62 | 63 | DeprecatedHashMethods: 64 | Enabled: false 65 | 66 | Documentation: 67 | Enabled: false 68 | 69 | DotPosition: 70 | EnforcedStyle: trailing 71 | 72 | DoubleNegation: 73 | Enabled: false 74 | 75 | EachWithObject: 76 | Enabled: false 77 | 78 | EmptyLiteral: 79 | Enabled: false 80 | 81 | Encoding: 82 | Enabled: false 83 | 84 | EvenOdd: 85 | Enabled: false 86 | 87 | FileName: 88 | Enabled: false 89 | 90 | FlipFlop: 91 | Enabled: false 92 | 93 | FormatString: 94 | Enabled: false 95 | 96 | GlobalVars: 97 | Enabled: false 98 | 99 | GuardClause: 100 | Enabled: false 101 | 102 | IfUnlessModifier: 103 | Enabled: false 104 | 105 | IfWithSemicolon: 106 | Enabled: false 107 | 108 | Lambda: 109 | Enabled: false 110 | 111 | LambdaCall: 112 | Enabled: false 113 | 114 | LineEndConcatenation: 115 | Enabled: false 116 | 117 | LineLength: 118 | Max: 80 119 | 120 | MethodLength: 121 | Enabled: false 122 | 123 | ModuleFunction: 124 | Enabled: false 125 | 126 | NegatedIf: 127 | Enabled: false 128 | 129 | NegatedWhile: 130 | Enabled: false 131 | 132 | NilComparison: 133 | Enabled: false 134 | 135 | Not: 136 | Enabled: false 137 | 138 | NumericLiterals: 139 | Enabled: false 140 | 141 | OneLineConditional: 142 | Enabled: false 143 | 144 | OpMethod: 145 | Enabled: false 146 | 147 | ParameterLists: 148 | Enabled: false 149 | 150 | PercentLiteralDelimiters: 151 | PreferredDelimiters: 152 | '%': '{}' 153 | 154 | PerlBackrefs: 155 | Enabled: false 156 | 157 | PredicateName: 158 | NamePrefixBlacklist: 159 | - is_ 160 | 161 | Proc: 162 | Enabled: false 163 | 164 | RaiseArgs: 165 | Enabled: false 166 | 167 | RegexpLiteral: 168 | Enabled: false 169 | 170 | SelfAssignment: 171 | Enabled: false 172 | 173 | SingleLineBlockParams: 174 | Enabled: false 175 | 176 | SingleLineMethods: 177 | Enabled: false 178 | 179 | SignalException: 180 | Enabled: false 181 | 182 | SpecialGlobalVars: 183 | Enabled: false 184 | 185 | StringLiterals: 186 | EnforcedStyle: double_quotes 187 | 188 | VariableInterpolation: 189 | Enabled: false 190 | 191 | TrailingComma: 192 | Enabled: false 193 | 194 | TrivialAccessors: 195 | Enabled: false 196 | 197 | VariableInterpolation: 198 | Enabled: false 199 | 200 | WhenThen: 201 | Enabled: false 202 | 203 | WhileUntilModifier: 204 | Enabled: false 205 | 206 | WordArray: 207 | Enabled: false 208 | 209 | # Lint 210 | 211 | AmbiguousOperator: 212 | Enabled: false 213 | 214 | AmbiguousRegexpLiteral: 215 | Enabled: false 216 | 217 | AssignmentInCondition: 218 | Enabled: false 219 | 220 | ConditionPosition: 221 | Enabled: false 222 | 223 | DeprecatedClassMethods: 224 | Enabled: false 225 | 226 | ElseLayout: 227 | Enabled: false 228 | 229 | HandleExceptions: 230 | Enabled: false 231 | 232 | InvalidCharacterLiteral: 233 | Enabled: false 234 | 235 | LiteralInCondition: 236 | Enabled: false 237 | 238 | LiteralInInterpolation: 239 | Enabled: false 240 | 241 | Loop: 242 | Enabled: false 243 | 244 | ParenthesesAsGroupedExpression: 245 | Enabled: false 246 | 247 | RequireParentheses: 248 | Enabled: false 249 | 250 | UnderscorePrefixedVariableName: 251 | Enabled: false 252 | 253 | Void: 254 | Enabled: false 255 | --------------------------------------------------------------------------------