pre-commitへPull Request送ったときの調査方法
pre-commitって何?という人は、こちらを先に見てください
もしや、バグ?
ignoreできないファイルがある
普段から、pre-commitを愛用していますが、中にはチェックして欲しくないファイルもあります。そんなときは、.pre_commit.ignoreにファイルのパターンを記載するのですが、どうしてもignoreできないケースがありました。
# .pre_commit.ignore foo/*/*.yml
fooディレクトリ以下にある、YAMLファイルは対象外
# foo/bar/baz.yml key: val
実際、余計な改行が入っている
$ pre-commit run pre-commit: Stopping commit because of errors. foo/bar/baz.yml:2: new blank line at EOF.
無視されず、怒られた orz
ちょっとコードを追って見る
rbenvでRubyを管理しているので、.rbenvディレクトリ以下にGemがインストールされています。RubyのVersionは適宜読み替えて下さい。
$ cd ~/.rbenv/versions/2.0.0-p598/lib/ruby/gems/2.0.0/gems/pre-commit-0.22.1/
bin以下に実行ファイルがある
# bin/pre-commit #!/usr/bin/env ruby require 'pre-commit/cli' if !File.exists?(".git") abort "No .git directory found." end PreCommit::Cli.new(*ARGV).execute or exit 1
ARGVで引数を渡している
module PreCommit class Cli def initialize(*args) @args = args end def execute() action_name = @args.shift or 'help' action = "execute_#{action_name}".to_sym if respond_to?(action) then send(action, *@args) else execute_help(action_name, *@args) end end def execute_run(*args) require 'pre-commit/runner' PreCommit::Runner.new.run(*args).tap { |ok| puts "No failed checks." if ok } end end end
action_name 今回は、runなので execute_run が呼ばれる
# lib/pre-commit/runner.rb module PreCommit class Runner include PreCommit::Utils::StagedFiles def run(*args) set_staged_files(*args) run_single(:warnings) run_single(:checks ) or return false true end end end
runメソッドでは、まずはcommit対象のファイルを探してそう。
set_staged_filesメソッドは、このクラスに無いので
include PreCommit::Utils::StagedFiles
で、読み込んでるのかな?
そろそろ、追うの大変になってきたので、binding.pryを仕込む
# lib/pre-commit/runner.rb require 'beybug' require 'pry-beybug' module PreCommit class Runner include PreCommit::Utils::StagedFiles def run(*args) binding.pry set_staged_files(*args) run_single(:warnings) run_single(:checks ) or return false true end end end
beybugとpry-beybugをrequireして、止めたい箇所に binding.pry を挿入
$ pre-commit run Frame number: 0/5 From: /Users/takeshi.takizawa/.rbenv/versions/2.0.0-p598/lib/ruby/gems/2.0.0/gems/pre-commit-0.22.1/lib/pre-commit/runner.rb @ line 28 PreCommit::Runner#run: 26: def run(*args) 27: binding.pry => 28: set_staged_files(*args) 29: run_single(:warnings) 30: run_single(:checks ) or return false 31: true 32: end
step実行で先に進めていく
From: /Users/takeshi.takizawa/.rbenv/versions/2.0.0-p598/lib/ruby/gems/2.0.0/gems/pre-commit-0.22.1/lib/pre-commit/utils/staged_files.rb @ line 48 PreCommit::Utils::StagedFiles#filter_files: 40: def filter_files(files) 41: files.reject do |file| 42: !File.exists?(file) || 43: File.directory?(file) || 44: ( 45: size = File.size(file) 46: size > 1_000_000 || (size > 20 && binary?(file)) 47: ) || => 48: repo_ignores.any? { |ignore| File.fnmatch?(ignore, file) } 49: end 50: end [4] pry(#<PreCommit::Runner>)> repo_ignores.any? { |ignore| File.fnmatch?(ignore, file) } => true [5] pry(#<PreCommit::Runner>)> file => "foo/bar/baz.yml"
ちゃんと、ignore対象になっているみたい。ignoreファイルの書き方はあってそう。
ソースコードに戻ると、listをひとつひとつ実行して、エラーが一つでもあれば show_output でエラー表示しつつ false 返している。
def run_single(name) show_output(name, execute(list_to_run(name))) end def show_output(name, list) if list.any? @stderr.puts send(name, list) return false end true end def execute(list) list.map do |cmd| result = nil seconds = Benchmark.realtime do result = cmd.new(pluginator, config, list).call(staged_files.dup) end puts "#{cmd} #{seconds*1000}ms" if debug result end.compact end
どのチェックに引っかかったか分かるように、デバッグメッセージ埋め込む。
def execute(list) list.map do |cmd| result = nil seconds = Benchmark.realtime do result = cmd.new(pluginator, config, list).call(staged_files.dup) end puts "#{cmd} #{seconds*1000}ms" if debug unless result.nil puts "=" * 30 puts "#{cmd} #{result}" puts "=" * 30 end result end.compact end
$ pre-commit run ============================== PreCommit::Checks::Whitespace foo/bar/baz.yml:2: new blank line at EOF. ============================== pre-commit: Stopping commit because of errors. foo/bar/baz.yml:2: new blank line at EOF. pre-commit: You can bypass this check using `git commit -n`
PreCommit::Checks::WhitespaceでCheckが通らなかった事がわかった。
"class Whitespace"でgrepして定義位置探す。
# lib/plugins/pre_commit/checks/whitespace.rb module PreCommit module Checks class Whitespace < Plugin def call(staged_files) binding.pry errors = `git diff-index --check --cached HEAD -- #{files_string(staged_files)} 2>&1` return if $?.success? # Initial commit: diff against the empty tree object if errors =~ /fatal: bad revision 'HEAD'/ errors = `git diff-index --check --cached 4b825dc642cb6eb9a060e54bf8d69288fbee4904 -- 2>&1` return if $?.success? end errors end end end end
call直後で止めてみる
$ pre-commit run Frame number: 0/11 From: /Users/takeshi.takizawa/.rbenv/versions/2.0.0-p598/lib/ruby/gems/2.0.0/gems/pre-commit-0.22.1/lib/plugins/pre_commit/checks/whitespace.rb @ line 27 PreCommit::Checks::Whitespace#call: 25: def call(staged_files) 26: binding.pry => 27: errors = `git diff-index --check --cached HEAD -- #{files_string(staged_files)} 2>&1` 28: return if $?.success? 29: 30: # Initial commit: diff against the empty tree object 31: if errors =~ /fatal: bad revision 'HEAD'/ 32: errors = `git diff-index --check --cached 4b825dc642cb6eb9a060e54bf8d69288fbee4904 -- 2>&1` 33: return if $?.success? 34: end 35: 36: errors 37: end [1] pry(#<PreCommit::Checks::Whitespace>)> staged_files => [] [2] pry(#<PreCommit::Checks::Whitespace>)> "git diff-index --check --cached HEAD -- #{files_string(staged_files)} 2>&1" => "git diff-index --check --cached HEAD -- 2>&1"
あぁ、本来は対象のファイルを指定して git diff-index --check するのだけれど、staged_filesが空の場合はファイル指定無し、つまり、全ファイルがチェック対象になっちゃうのね。
空の時はチェックしなければ良いじゃない
後始末
$ gem pristine pre-commit Restoring gems to pristine condition... Restored pre-commit-0.22.1
pre-commitを初期状態に戻す。