仕事でGitのコミットメッセージをちゃんと書けているか?ゲーム

仕事のコミットメッセージは雑になっていることが多い。

それを解決する方法を考えていたらふと思いついたので、ブログに書いておく。

基本ルール

  1. 後述するコマンドでGitのコミットを 5つ 取得する
  2. git show コマンドでコミットのログとdiffを表示する
  3. そのまま変更内容を説明できれば 5点
    • 少なくとも「なぜ必要だったのか?」と「その変更は現在も必要か?」くらいは説明して欲しい
  4. コミットメッセージから外部URLを開くと -1点
    • リンク先で外部URLを開くと 更に-1点
  5. コミットに関連するプルリクの概要、コミット一覧を閲覧したら -2点
  6. コミットの author に質問する場合 -3点
  7. コミットの author が退職者でもう聞けない場合 -5点
  8. 5つのコミットで点数を計算し、足した値が合計点

バグ対応で関係あるコミットを見つけたとき、そのコミットの意図を調べる時間が短いほど高得点...みたいな意図で点数を配分してます。

普通の遊び方

以下のように自分のユーザー名を author で指定しつつ、3ヶ月前のコミットをランダムで5つ選ぶ。

$ git log --author=sinsoku \
  --since="4 months ago" --until="3 months ago" \
  --no-merges --oneline | sort --random-sort | head -n 5

「3ヶ月前の自分は他人」という言葉もあるけど、自分のコミットは説明できるかどうか?

定期的に合計点を計測しておけば、自分のコミットメッセージの改善を定量的に測れるかも。

チームで遊ぶ

authorを指定しないことでリポジトリにコミットしていた人の全員を対象にできます。
(コミット数は5よりもメンバー数にして、1人1コミットを説明する方が良さそう)

$ git log \
  --since="4 months ago" --until="3 months ago" \
  --no-merges --oneline | sort --random-sort | head -n 5

文字通り「他人の書いたコミット」を git show で表示し、その人の前で説明するのは面白いと思う。

ただ、"レビュー指摘修正" みたいな雑コミットが当たると気まずくなる可能性はある。

チーム対抗戦

2つのリポジトリ(2つのチーム)で合計点を競う。

  • コミットの説明を他チームに対して行う
  • 他チームの人が別チームのコミットの説明をする

などチームのメンバーを混ぜると面白いと思う。

これ読んで「他チームの仕様を説明できる必要があるのか?」と思う人がいると思うけど、いきなり別チームに異動することはある。
人事異動はいつだって突然降ってくる。

まとめ

深夜に思いついたネタを適当に書いた。

仕事のリポジトリで試してみたい。

Gitの書籍に出てこないようなGitの使い方を2つ紹介

Twitterの140文字だと説明しづらいので、ブログに書く。

ケース1: 過去のコミットを部分的に戻す

概要

a001から分岐してb001-003まで3つコミットをした後、b001のコミットを修正したいケース。

f:id:sinsoku:20200211234320p:plain

よくある方法

git rebase -i を使って、b001のコミットを edit にする。

詳細はググれば出てくるので省略。

便利: 相殺コミットをぶつける方法

b001の一部を修正したコミットb004を用意します。

f:id:sinsoku:20200211235128p:plain

git rebase -i a001 すると以下のような状態でエディタが開く。

pick a001 hello
pick b001 foo
pick b002 bar
pick b003 buz
pick b004 fix_foo

これの順番を入れ替えて、b004をfixupに変える。

pick a001 hello
pick b001 foo
fixup b004 fix_foo
pick b002 bar
pick b003 buz

保存してエディタを閉じると、b001のコミットにb004のコミットを混ぜる(一部を戻す)ことができる。

メリット

  • 途中でコンフリクトしたときにgit rebase --abort で戻れる
  • commit --fixup と組み合わせるとより便利
  • 修正コミットは git checkout <sha1> -- <path> などで作っても良い

過去のツイート

ケース2: 作業中のファイルを一時的に退避する

a001から分岐したfeatureブランチでb001のコミットをした後、別の作業を依頼されたケース。

f:id:sinsoku:20200212002030p:plain

よくある方法

別ブランチをa001から切って、作業を進める。

便利: 一時的にリバートする

とりあえずb001をリバート(b001')して、別の作業を済ませて c001 のコミットを作ります。

f:id:sinsoku:20200212002638p:plain

次にc001だけを持つブランチを作ります。

$ git switch -c fix_bug
$ git rebase -i a001
# エディタが開くので、c001 のコミットだけ残す

fix_bugのブランチでプルリクを作ったら、元のブランチに戻ってリバートコミットを取り除きます。

$ git switch feature_1
$ git rebaes -i a001
# エディタが開くので、b001'とc001のコミットを取り除く

最終的に以下のようなグラフになって、b001から作業を再開できる。

f:id:sinsoku:20200212002647p:plain

メリット

  • ブランチの切り替えが減る
  • DBのスキーマを変えずに済む
    • テストデータなども消えない
  • ライブラリのバージョンを変えずに済む
    • masterでgemが更新されていると bundle-install に時間がかかるけど、これを回避できる
  • コンフリクトするコミットだけをリバートするだけなので簡単
    • ブランチの全コミットをリバートする必要はない

過去のツイート

まとめ

ブログに書いてみたけど、やっぱり分かりづらい。

別のディレクトリにある Gemfile をsystemメソッドで "bundle check" する

タイトルの通りなのですが、普通にやると意図した動きしなかったのでメモ。

具体的な例

factory_bot が分かりやすいです。

具体的なコマンドは以下の通り。

$ git clone --depth 1 https://github.com/thoughtbot/factory_bot.git
$ cd factory_bot
$ bundle install
$ bundle exec ruby -e 'system({ "BUNDLE_GEMFILE" => "6.0.gemfile" }, "bundle check", chdir: "gemfiles/")'
Could not find zeitwerk-2.1.9 in any of the sources
Run `bundle install` to install missing gems.

動くコマンド

bundler経由で実行すると RUBYOPT="-r/usr/local/lib/ruby/2.7.0/bundler/setup" がセットされるので、これを上書きすると動く。

$ bundle exec ruby -e 'system({ "BUNDLE_GEMFILE" => "6.0.gemfile", "RUBYOPT" => ""}, "bundle check", chdir: "gemfiles/")'
The following gems are missing
 * zeitwerk (2.1.9)
 * childprocess (2.0.0)
 * aruba (0.14.11)
 * simplecov (0.17.0)
Install missing gems with `bundle install`

bundle install する前なので、正しく 6.0.gemfile を参照してる挙動になった。

追記

Bundler.with_clean_system を使う方法を Twitter で教えてもらった。

こっちの方がシンプルで、良いですね!

PostgreSQL 12でpg_hint_planをインストールする方法

brew install postgresql でインストールした postgres で pg_hint を使えるようにする方法が分からなかったのでメモ。

URL一覧

インストール方法

OSDN の v1.3.4 の rpm に含まれているのは Postgres 11 用らしく、 Postgres 12 で動くか不安だったので使うのは避けた。

そして、 v1.3.4 のソースも Postgres 11 用なのか、ビルドはエラーになった。

$ make
clang -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -Wno-unused-command-line-argument -O2  -I. -I./ -I/usr/local/include/postgresql/server -I/usr/local/include/postgresql/internal -I/usr/local/Cellar/icu4c/64.2/include -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -I/usr/local/opt/openssl@1.1/include -I/usr/local/opt/readline/include -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/libxml2   -c -o pg_hint_plan.o pg_hint_plan.c
pg_hint_plan.c:20:10: fatal error: 'nodes/relation.h' file not found
#include "nodes/relation.h"
         ^~~~~~~~~~~~~~~~~~
1 error generated.
make: *** [pg_hint_plan.o] Error 1

GitHubのmasterブランチ

GitHub からソースを取得し、 22a770c で make すると、今度は別のエラーが出た。

$ make
clang -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -Wno-unused-command-line-argument -O2  pg_hint_plan.o -L/usr/local/lib -L/usr/local/opt/openssl@1.1/lib -L/usr/local/opt/readline/lib  -Wl,-dead_strip_dylibs  -Wl,--build-id  -bundle -bundle_loader /usr/local/Cellar/postgresql/12.1/bin/postgres -o pg_hint_plan.so
ld: unknown option: --build-id
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [pg_hint_plan.so] Error 1

よく分からないけど --build-id のオプションがダメらしいので、消してみたら make と make install が実行できた。

$ git diff
diff --git a/Makefile b/Makefile
index de9f5c5..bde9cdc 100644
--- a/Makefile
+++ b/Makefile
@@ -31,7 +31,7 @@ TARSOURCES = Makefile *.c  *.h COPYRIGHT* \
        doc/* expected/*.out sql/*.sql sql/maskout.sh \
        data/data.csv input/*.source output/*.source SPECS/*.spec

-LDFLAGS+=-Wl,--build-id
+LDFLAGS+=-Wl

 installcheck: $(REGRESSION_EXPECTED)

これで pg_hint_plan は使えるようになったけど、これが正式なやり方かは謎。

銀座Railsで「Railsアプリの設計」というタイトルで発表しました #ginzarails

12/13(金)にあった銀座Rails#16で登壇しました。

スライド

speakerdeck.com

話したかったこと

  • 1〜3年目のRailsエンジニアに参考になる話がしたい
    • 7年やって私が覚えたことは、たぶん何かしら参考になるだろう
    • 来週の仕事ですぐ参考になる話がしたかった
  • 懇親会で他のシニアエンジニアの考えを聞きたかった

補足: なぜクラス内クラスを使うのか?

スライドの最後の方が説明不足だったので、ブログで補足を書いておきます。

👮‍♀️RuboCop の取締りは厳しい

RuboCop の標準ルールを守ろうとすると、以下を行うことになる。

  1. 行数が長くなると、変数を抽出する
  2. 変数代入によって AbcSize が増えるので、プライベートメソッドを抽出する
  3. メソッド追加によって ClassLength が増える

対策方法は2つある

  • Module を抽出する
  • Class を抽出する

しかし、Module はプライベートメソッドを使えない。(スライドを参照)

Class を抽出する

  • 普通にクラスを作る
  • 内部クラス(クラス内クラス)を作る

内部クラスだと僅かにですが使用箇所を制限できる。

例えば、スライド例で挙げた User::Ranking を他のクラス(例えば Blog )で使うのは避けるはず。

これにより User::Ranking を消すときに調べる範囲を減らして、消しやすくしている。

結論

このような思考過程で内部クラス(クラス内クラス)を利用したクラス設計を好んで使っています。

ActiveRecord::Base.unscope の呼び出しでネストが深くなっているのを浅くする

仕事で見かけたコードを汎用化した内容にして、個人ブログにメモっておく。*1

多段ネストを reverse して Proc でラップしたり、それを RSpec で検証する方法はブログ書いてないと忘れる自信がある。

モデルの定義

class User < ApplicationRecord
  default_scope { where(deleted_at: nil) }
end

class Article < ApplicationRecord
  default_scope { where.not(published_at: nil) }
end

元コード

User.unscoped do
  Article.unscoped do
      assert_equal 'SELECT "users".* FROM "users"', User.all.to_sql
      assert_equal 'SELECT "articles".* FROM "articles"', Article.all.to_sql
  end
end

書き換えたあと

module ScopeHelper
  def self.unscoped(models:, &block)
    models.reverse
      .inject(block) { |result, model| -> { model.unscoped(&result) } }
      .call
  end
end

ScopeHelper.unscoped(models: [User, Article]) do
  assert_equal 'SELECT "users".* FROM "users"', User.all.to_sql
  assert_equal 'SELECT "articles".* FROM "articles"', Article.all.to_sql
end

RSpecで書いたテスト

RSpec.describe ScopeHelper do
  describe '.unscoped' do
    it do
      expect(User).to receive(:unscoped).and_call_original
      block = -> {}
      expect(Article).to receive(:unscoped) do |&args|
        expect(args).to eq block
      end

      ScopeHelper.unscoped(models: [User, Article], &block)
    end
  end
end

*1:default_scopeとか絶対使わないけど、他に汎用化した例は思い浮かばなかった

2019年11月現在のRailsのissueに関するメモ #heiseirb

平成.rb #10Railsのissueに取り組むきっかけが分からない人向けのメモです。

個人的に気になっているissueについて簡単にまとめました。

注意)特に初心者向けとかではありません。

STIhas_secure_password の組合せで、親クラスでvalidationsを切り替えたい?

Trouble using has_secure_password validations attribute with STI models https://github.com/rails/rails/issues/37755

  • 再現コードなし
  • 登録者のコードは意図通りじゃなさそう
    • self.classClass を返すから常に true

scope + new でエラーが出る

raise_on_type_mismatch! in ActiveRecord::Associations::BelongsToAssociation https://github.com/rails/rails/issues/37752

  • 再現コードなし
  • コミッターの反応あり

ignored_columns + from で意図しないカラムが無視される

Ignored columns is ignoring virtual columns selected by .from https://github.com/rails/rails/issues/37745

  • 再現コードなし

through と polymorphic の組合せで外部キーがnilになる

Assigning with a through association and a self-ref polymorphic association leaves a nil foreign key https://github.com/rails/rails/issues/37758

  • 再現コードなし

request.variant の挙動が5.2と6.0で違う

Rails 6.0.0 chooses variants differently from 5.2 when variants are using different template engines https://github.com/rails/rails/issues/37021

  • 再現コードなし
  • リグレッション
    • 5.2 の挙動がバグっていた可能性もある
  • stale ラベル

enum で文字列を代入したらエラーになる

Revisiting the inconsistency in enums: raises ArgumentError when making an assignment with invalid value, but returns wrong results when querying https://github.com/rails/rails/issues/37630

  • 再現コードなし

accepts_nested_attributes_for でモデルを削除してもメモリ上に残る

Removing association with dependent: :destroy, through nested attributes leaves entity from intermediate table in memory https://github.com/rails/rails/issues/37649

  • 再現コードあり

DateTime#advance の挙動が5.2と6.0で違う?

DateTime#advance() expats integer params https://github.com/rails/rails/issues/37425

  • 再現コードなし

joins と pluck をあわせて使うと型が変わる

Unexpected Type Casting in ActiveRecord::Calculations#pluck https://github.com/rails/rails/issues/28044

  • 再現コードあり
  • 直すのは難しそう

テーブルない状態で order を呼ぶと5.2からエラー?

Calling "order" crashes when the model isn't represented by a database table https://github.com/rails/rails/issues/37741

  • 再現コードあり
  • 仕様かバグかの判断から必要そう

belongs_to と scope の組合せでsaveに失敗する

belongs_to with scope cause racord save fails https://github.com/rails/rails/issues/36990

  • 再現コードなし
  • stale ラベル