【git hooks】pre-pushで特定のブランチへの誤pushを防止するようにした

はじめに

リリース時にのみmasterにpushすることがあるんですが、これをリリース時以外にやってしまうとrevert操作などが必要になって結構面倒なことになります。

そこで、git hooksのpre-pushフックを使って特定のブランチへのpushの際に確認メッセージを表示するように設定しました。(chatGPTの力も存分に借りながら)

設定方法

1. .git/hooks/git-pushファイルを作成する
2. 設定を記載する

以下の設定では、masterdevelopにpushするときに、[WARNING] push to ブランチ名, are you sure? (Y/n)といったメッセージが表示され、Yを入力した際にのみpushがされます。

#!/bin/sh

protected_branches="master develop"
current_branch=$(git branch --show-current)

for branch in $protected_branches; do
  if [[ "$current_branch" == "$branch" ]]; then
    echo "[WARNING] push to $branch, are you sure? (Y/n)"
    exec < /dev/tty
    read answer

    if [[ $answer != "Y" ]]; then
      echo "Push canceled"
      exit 1
    fi
  fi
done

exit 0
3. 実行権限を付与

作成したファイルに実行権限を付与します。

$ chmod +x .git/hooks/pre-push

以上で、設定は完了です。

確認

例えば、nを入力するとpushはキャンセルされます。

$ git push
[WARNING] push to master, are you sure? (Y/n)
n
Push canceled

逆に、Yを入力すればpushされます。

おわりに

chatGPTに言われた方法だと、readコマンドを使う方法で、それを元に色々試してみたのですが、うまくいかなかったです。(pre-pushで行うのも関係している、、?と思いましたが深追いはしていません)

ググっていたら以下の記事に出会って、readコマンドではなく、exec < /dev/ttyで行うようにしました。

  • 参考記事

dev.classmethod.jp

Railsガイドにきちんと目を通して新しい知識を得る - Rails のスレッドとコード実行編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、Rails のスレッドとコード実行の章です。

railsguides.jp

ExecutorとReloader

リンクはこちら

理解するのが難しかった(今でも完全には理解できていない)ですが、Kaigi on Rails 2023の動画がわかりやすかったです。

kaigionrails.org

  • Executorはフレームワークとアプリケーションのコードを区別するもの
  • これがあるのでDBコネクションやキャッシュクリアを意識せずに開発できる
  • 例えばアクションを実行する際、Executorは、実行前にto_runを、実行後にto_completeを呼び出す
  • Executorはリソース管理のみ
  • ReloaderはExecutorをwrapした上で、アプリケーションコードを実行する前に最新のコードが読まれているかを確認する
  • 例えば、gemからアプリケーションコードを読むとき、リソース管理とコードの再読み込みをするには、Rails.application.reloader.wrapを使う

permit_concurrent_loads

リンクはこちら

Railsガイドのサンプルコードの理解

Rails.application.executor.wrap do
  th = Thread.new do
    Rails.application.executor.wrap do
      User # 内側のスレッドはここで待機する
           # 他のスレッドが実行中はUserを読み込めない
    end
  end

  th.join # 外側のスレッドは'running'ロックをつかんだままここで待機する
end
  • 「外側のスレッド」: 1行目のRails.application.executor.wrapを実行している
  • 「内側のスレッド」: Thread.newによって生成された新たなスレッド
  • join: 特定のスレッド(今回はth)の実行が完了するまで、それを呼び出したスレッド(外側のスレッド)の実行を一時停止する

=> Userを読み込むためには外側のrunningロックが解放されないといけないが、外側のスレッドはth.joinで内側のスレッドの終了を待って一時停止しているのでデッドロックになる

これに対して、permit_concurrent_loadsを使うことでそのスレッドでは、提供されたブロック内で自動読み込みされた可能性のある定数を参照解決しないことが保証され、ある意その定数の読み取りを他スレッドに「許可」できる。

Railsガイドにきちんと目を通して新しい知識を得る - Rails のキャッシュ機構編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、Rails のキャッシュ機構の章です。

railsguides.jp

条件付きGET

リンクはこちら

stale?を使うことで条件付きGETを実現できます。

stale?を使ってキャッシュしたページを見せる場合、ステータスコード304 Not Modifiedが返ってくることを確認できました。

Started GET "/books/1"
Processing by BooksController#show as HTML
  ...
Completed 304 Not Modified in 40ms (ActiveRecord: 3.1ms | Allocations: 5132)

開発環境のcacheのON/OFF

リンクはこちら

dev:cacheを使うことで、開発環境で自由にcacheをON/OFFできます。

$ rails c
irb> Rails.cache.write('sample_key', 'sample_value')
irb> Rails.cache.read('sample_key')
#=> nil

$ rails dev:cache
#=> Development mode is now being cached.
$ rails c
irb> Rails.cache.write('sample_key', 'sample_value')
irb> Rails.cache.read('sample_key')
#=> "sample_value"

Railsガイドにきちんと目を通して新しい知識を得る - Rails の自動読み込みと再読み込み編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、Rails の自動読み込みと再読み込みの章です。

railsguides.jp

config.autoload_paths

リンクはこちら

app/下のディレクトリは自動読み込みされる。

例えばapp/samples/dummy.rbの中でDummyクラスを定義すると、rails consoleで起動した際などに読み込みができる。

一方で、例えばルートディレクトリ直下にsamples/dummy.rbというファイルを作った場合、samplesディレクトリは自動読み込みの対象外なのでDummyクラスは見つからない。

この場合に、config.autoload_pathsに登録することで自動読み込みの対象にできる。

# application.rb
config.autoload_paths << "#{root}/samples"

一度しか自動読み込みしないconfig.autoload_once_paths

リンクはこちら

アプリケーション起動時の一度だけ読み込まれるファイルを指定します。

config.enable_reloading = trueとの関係はどうなるんだろう?と思って開発環境で試してみました。

# development.rb
config.enable_reloading = true
config.autoload_once_paths << "#{root}/samples"

仮に上記のように設定した場合、samples配下のファイルを修正してリロードしても修正が反映されませんでした。

reload!

リンクはこちら

reloadをするとクラスが別物になります。アプリケーション内でreload!を入れることはあんまりなさそうですが、知っておくとハマりポイントを1つ回避できそうですね。

book_1 = Book.new
reload!
book_2 = Book.new

book_1.class.object_id
#=> 245440
book_2.class.object_id
#=> 462620

初期化時にリロード可能な定数を参照することは禁止

リンクはこちら

あまり使うケースがなく知らなかったのですが、config/initializers内でActiveRecordのクラスを読み込むなどしようとするとNameErrorで失敗します。

# config/initializers/sample.rb
p Book.new
rails s
# NameError

コードの変更(今回の例だとBook)があった場合ごとに読み込みたい場合はconfig.to_prepareを使えます。

# config/initializers/sample.rb
Rails.application.config.to_prepare { p Book.new }

もしくは、初期化時の1度だけ読み込めば良い時はafter_initializeを使えます。

# config/initializers/sample.rb
Rails.application.config.after_initialize { p Book.new }

Zeitwerkのcollapsing(折り畳み)機能

リンクはこちら

STI継承をするときなどに使えるメソッドとしてcollapsingが紹介されています。

これはディレクトリ構造をクラス名の名前空間に反映させたくなり場合などに使えます。

# app/models/samples/dummy.rb
class Dummy; end

上記だと、本来期待されるのはSamples::Dummyなので呼び出そうとしてもNameErrorが発生します。

これを例えばコンソールでcollapsingを試すと以下のようになります。

Dummy
#=> NameError)
Rails.autoloaders.main.collapse("#{Rails.root}/app/models/samples")

reload!

Dummy
#=> Dummy

push_dirでカスタム名前空間を作成する

リンクはこちら

collapseとは逆に、デフォルトの名前空間ディレクトリなしに作成したい時にはpush_dirが使えます。
Railsガイドに記載の通り、サービスクラスのクラスに対して、Services::名前空間を必要にしたい場合、ディレクトリ構成をapp/services/services/xxxとしなくても実現できます。

Railsガイドにきちんと目を通して新しい知識を得る - アセットパイプライン編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、アセットパイプラインの章です。

railsguides.jp

Sprocketsの利用法

リンクはこちら

マニフェストファイルとディレクティブ

manifest.jslink_directory ../stylesheets .cssで、app/assets/stylesheetsディレクトリのcssファイルを読み込みます。
実際に、この行を削除するとcssが効かなくなりました。

Railsガイドには、(ただしサブディレクトリは含めません)と記載があったのですが、手元でapp/assets/stylesheets/books/index.csscssファイルを書いたらCSSが反映されます。
これはどうしてだろうと思ったら、app/assets/stylesheets/application.cssに以下の記載がありました。

 *= require_tree .

これによって、ディレクトリ配下のcssファイルを読み込んでいます。
manifast.jslink_directoryを使うことでstylesheet_link_tagapplication.cssが読み込めるようになり、
application.css*= require_tree . とすることでapplication.cssからディレクトリ内のcssが読み込まれるということだと理解しました。

なお、application.cssには以下も記載されていました。

 *= require_self

これは、application.css自身を読み込むタイミングを決めるものです。
例えば*= require_tree .より上に書いた場合、配下のディレクトリ内のcssと対象が重複した場合は配下のcssが適用されます。
逆に、*= require_tree .より下に書いた場合は、application.css`内のcssが適用されます。

Sprocketsにおけるindex.css

例えばapplication.css*= require booksと記述したとき、books/index.cssが存在しないとエラーを返します。

Sprockets::FileNotFound in Books#index

そして、そのindex.css内で *= require_tree .を記載すると、booksディレクトリ内の他のcssファイルを読み込めます。

.css.erb

.css.erbファイルを使うことで、asset_data_uriヘルパーを使ってデータURLスキームを使うこともできるようになります。

Railsガイドにきちんと目を通して新しい知識を得る - Rails アプリケーションの設定項目編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、Rails アプリケーションの設定項目の章です。

railsguides.jp

rails実行前にコードを実行する

リンクはこちら

require 'rails/all' の上に書くことで、Railsが読み込まれる前にプログラムを実行できるそうです。
あまりユースケースが思いつかないですが、いざという時はありそうなので覚えておきたいです。

Rails全般の設定

リンクはこちら

週初めの設定をconfig.beginning_of_weekで設定する

config.beginning_of_weekで週の初めを何曜日とするかを設定できます。

デフォルト設定の場合、例えばDate.today.beginning_of_weekだと、今日が属する週の月曜日の日付インスタンスを返します。
(日曜かなと思っていたのですが、月曜でした。)

これを、config.beginning_of_weekを設定することで変更できます。

# appllication.rb

config.beginning_of_week = :wednesday
Date.today.beginning_of_week
#=> Wed, 14 Feb 2024

config.colorize_loggingでログの出力に色付けをする

これも設定になっていたとは知りませんでした。
falseにしたら確かにSQLクエリログなどに色がつかなくなりました。

config.disable_sandboxでsandboxの利用をさせない

sandbox環境の制限ができるのも知りませんでした。
データベースサーバーのメモリが枯渇するのを避けるうえで有用とのことです。
一方で、プログラムが問題なく動くか、や間違って更新しても安心なようにsandbox環境を使うことは多いと思うので、なかなか判断が難しいところですね。

これをtrueにしたところ、--sandboxオプションをつけてrailsコンソールを起動するとエラーが表示されるようになりました。

$ rails c --sandbox
Error: Unable to start console in sandbox mode as sandbox mode is disabled (config.disable_sandbox is true).

config.enable_reloadingでクラス等の変更時、リクエストごとに再読み込みするかどうかを決める

開発環境でコードの修正がリロードで反映されるのはconfig.enable_reloadingがtrueになっているからでした。
デフォルト設定では、production環境ではfalseというのも覚えておくべきだなと思いました。

config.sandbox_by_defaultでデフォルトをsandbox環境にする

Rails7.1から入った機能です。
config.sandbox_by_defaultをtrueにすると、railsコンソールがデフォルトでsandbox環境になります。
本番環境での事故防止に良さそうです。

手元の環境で試そうとしたら、trueに設定してもsandbox環境にならず、調べたところ、developmentとtestではこのオプションは無視されるとのことでした。

Note that this option is ignored when rails environment is development or test.

github.com

ActiveRecordを設定する

リンクはこちら

config.active_record.partial_inserts

trueにすると、createをしたときなどに、DBデフォルト値に対してはINSERTで値が送られなくなります。

設定をtrueにして、以下のbooksテーブルで試してみました。

カラム名 デフォルト値
title string 'デフォルトタイトル'
content string (設定なし)
Book.create
#=> INSERT INTO "books" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id"  [["created_at", "2024-02-18 ..."], ["updated_at", "2024-02-18 ..."]]

titleとcontentには確かにINSERTで値が指定されていません。
それぞれ'デフォルトタイトル'という文字列とnilが登録されていました。

こちらはRails7.0でデフォルトがfalseに変更されたそうです。
理由が気になって見てみたら、PRの方で以下の旨が挙げられていました。

  • ignored_columns機能が導入されたことにより、カラムを安全に削除する、より信頼性のある方法が生まれたこと
  • クエリサイズを減らすことの利点はそれほど大きなものではなくなった

github.com

active_record.before_committed_on_all_records

Rails7.1からのオプションです。

トランザクションに登録されているすべてのレコードに対して、before_committed!コールバックを有効にします。

上記の意味が最初よくわからなかったですが、試したらわかりました。

class Book < ApplicationRecord
  before_commit -> { p "title: #{title}" }
end

上記のように適当にbefore_commitを設定し、設定をfalseにします。

ActiveRecord::Base.transaction do
  book = Book.first
  book_2 = Book.first
  book.update(title: 'abc')
  book_2.update(title: 'def')
end

#=> "title: abc"

一方で、trueにした場合、pメソッドで出力される値は"title: def"になります。

これを見ると、falseにした場合、トランザクション内で同じレコードに対して複数回処理をした場合に、before_commitで見るのは最初に更新された方ということがわかりました。

ActionControllerを設定する

リンクはこちら

raise_on_open_redirectsでオープンリダイレクト脆弱性の対策を強化する

うっかりオープンリダイレクト脆弱性が紛れてしまった場合に、リダイレクト先が外部ホストの場合は例外を出してくれます。

例えば適当なコントローラで以下のように設定し、アクセスするとActionController::Redirecting::UnsafeRedirectErrorという例外が発生します。

allow_other_host: trueをつければ外部ホストへのリダイレクトが可能になる、というように回避策も用意されているので、基本はtrueで良さそうに思いました。

def index
  redirect_to 'http://example.com'
end

Railsガイドにきちんと目を通して新しい知識を得る - Rails アプリケーションのデバッグ編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、Rails アプリケーションのデバッグの章です。

railsguides.jp

SQLクエリコメント

リンクはこちら

ログに出力されるSQLクエリログに、どのコントローラのどのアクション起因かというのが表示されます。
SQL実行に無駄がないか(結果をキャッシュできる箇所がないか)など調査するのに便利そうですね。

config.active_record.query_log_tags_enabled = true
SELECT "books".* FROM "books" /
*action='index',application='Myapp',controller='books'*/  ← この情報が追記される

debug gemによるブレークポイント

リンクはこちら

breakで行数や実行コードを指定して処理を止める

binding.bdebuggerで処理を止めた後、break 18b 18で行数で処理を止められます。
また、b Book.newのようにすると、呼ばれたnewメソッドで処理が止まるようになります。

gemのソースコードを読んだりするときに今まではdebuggerで処理を止めたところからジャンプ先にまたdebuggerを設定して、、とやっていましたが、これを上手に使えば楽になりそうです。

watchインスタンスの変化があった場合に処理を止める

watchを使うことで、オブジェクトに変化があった場合に処理を止めてくれます。
ちなみにActiveRecordインスタンスを指定した際には、インスタンスの属性が変わっただけでは処理は止まりませんでした。

def index
  debugger # ここで止まった時に、`watch @book`とする
  @book = Book.first # nil から インスタンスが入るので処理が止まる
  @book.title = '変更'  # 属性が1つ変わるだけでは止まらない
  @book = :book # シンボル:bookに変わるので止まる
end