Git操作時に、事故が起きないように書いていたpre-push用のスクリプトをbashからRubyに書き換えました。
このスクリプトは、
- mainやmasterブランチに意図せずpushしてしまう
- 意図せずforce系のオプションを付けてpushしてしまう
といった事故を防ぐために設定しています。
GitHub側のリポジトリ設定で保護設定を有効化にしておけば、まずこのようなことはする必要ないですし、
そもそもこういった意図せずmainブランチにpushしてしまうといった操作自体も、まず起こりえません。
ですが、稀にあるエッジケースへの対策として私個人が設定しているものになります。
pre-push
スクリプトは、shebangでRubyとして実行されるようにしているpre-pushで、内容は以下のようになります。
#!/usr/bin/env ruby def main_branch?(branch_name) /\A(master|main)\z/.match? branch_name end def restrict_branches(branch_name) return if /\A(y|yes)\z/ =~ ENV['GIT_ALLOW_PUSH_MAIN_BRANCH'] fail "Don't push default branch!!! (master or main)" if main_branch? branch_name end def use_force_option?(command) /--force|-f/.match?(command) end def restrict_force_push(command) return if /\A(y|yes)\z/ =~ ENV['GIT_ALLOW_FORCE_PUSH'] fail "Don't use --force option!!!" if use_force_option? command end def main _, _, remote_ref, _ = STDIN.gets.chomp.split branch_name = remote_ref.gsub('refs/heads/', '') command = `ps -o command= -p #{Process.ppid}`.chomp restrict_branches branch_name restrict_force_push command rescue => e STDERR.puts e.message exit 1 end main if __FILE__ == $0
通常pre-pushなどのフックでは、ユーザが実行したコマンドのオプションは渡りません。 ですが、pre-pushが動く時の親プロセスが「ユーザが実行したコマンド」という性質を利用して、forceオプションが含まれいてるかどうかを判別しています。
command = `ps -o command= -p #{Process.ppid}`.chomp
もっと簡単にオプションを知る方法があれば是非教えてほしいです。
テスト
せっかくRubyに移行したので、比較的副作用が強くないようにメソッドの分割して、テストも書くようにしてみました。 rspecなどのGemを別途インストールするのは正直面倒だったので、minitestでそれっぽく書いています。
require_relative "../test_helper" load "#{git_root_path}/.config/git/hooks/pre-push" describe 'Git pre-push hook' do describe '#main_branch?' do context 'branch_name is main' do it 'be true' do assert main_branch? 'main' end end context 'branch_name is master' do it 'be true' do assert main_branch? 'master' end end context 'branch_name is develop' do it 'be false' do assert ! main_branch?('develop') end end end describe '#restrict_branches' do context 'branch_name is main' do it 'be fail' do assert_raises RuntimeError do restrict_branches 'main' end end end context 'branch_name is develop' do it 'be nothing' do restrict_branches 'develop' end end context 'branch_name is main && GIT_ALLOW_PUSH_MAIN_BRANCH=yes' do it 'be nothing' do current = ENV['GIT_ALLOW_PUSH_MAIN_BRANCH'] ENV['GIT_ALLOW_PUSH_MAIN_BRANCH'] = 'yes' restrict_branches 'main' ENV['GIT_ALLOW_PUSH_MAIN_BRANCH'] = current end end end describe '#use_force_option?' do context '--force in command' do it 'be true' do assert use_force_option? 'git push --force origin main' end end context '-f in command' do it 'be true' do assert use_force_option? 'git push --force origin main' end end context 'not force push command' do it 'be false' do assert ! use_force_option?('git push origin master') end end end describe '#restrict_force_push' do context '--force-with-lease in command' do it 'be nothing' do assert_raises RuntimeError do restrict_force_push 'git push --force-with-lease origin master' end end end context '-f in command && GIT_ALLOW_FORCE_PUSH=yes' do it 'be nothing' do current = ENV['GIT_ALLOW_FORCE_PUSH'] ENV['GIT_ALLOW_FORCE_PUSH'] = 'yes' restrict_force_push 'git push -f origin master' ENV['GIT_ALLOW_FORCE_PUSH'] = current end end context 'not force push command' do it 'be nothing' do restrict_force_push 'git push origin master' end end end end
Minitestを使うのは初めてなので、正直こういう書き方でいいのか、 やっぱりrspecって便利だなと改めて思うところです。