Google Meetで相手の画面やプレゼンテーションが見えないときの対処法
相手が見えない...
先日Google Meetでお話をする機会があったのですが、相手のプレゼンテーションやお顔がまったくみえないという事象に出くわしました...。
お相手の別の端末で会議に参加していただいたところ、すべて映っていたところから「これは自分側に問題があるぞ」と切り分けまでできたのですが、その時間では解決できず1時間音声+テキストで会話を続けました😅
終わってみて色々試してみたところ原因がわかったので、メモしておきます。
結論
設定 > 動画 > 受信時の解像度(最高)が「音声のみ」になっていると相手の動画全般が見えない。
「音声のみ」以外の選択肢に変更する
詳細
バージョン
確認したバージョンは以下の通りです。
設定:受信時の解像度(最高)
原因はGoogle Meetの受信時の設定にありました。設定 > 動画 > 受信時の解像度(最高)を確認します。
相手の画面やカメラが見えていない時は、ここの設定が「音声のみ」になっていました。これにより音声しか受信しなくなるので、相手の動画はすべて見えないというカラクリでした。
この設定は次回以降も保存されるそうなので、なにかの拍子にここを「音声のみ」にしてしまっていて、それが今回も引き継がれていたということになります。
終わりに
過去に「送信時の解像度(最高)」を高画質にしようとしたけど、カクツキすぎるので自動に戻したということがあって、おそらくその際に手が滑ったのでしょう...。
ネット上には「発信している側起因で見えない」ケースが結構転がっていますが、「受信する側起因で見えない」ケースはあまりなかったので、どなたかの参考になれば幸いです。
Vue3のReactivity TransformをJestでもコンパイルできるようにする
要約
Reactivity Transformを使ったコンポーネントをテストしていて、それが原因でJestが落ちるときは、@vue/vue-jest@27.0.0-alpha.3
以上を導入しよう!
yarn add -D @vue/vue3-jest@latest
環境
※この記事は2022/03/15書いたものです。執筆時点ではReactivity Transform自体がExperimentalかつ、vue-jestのアルファ版でしか対応していないため下記のような対応が必要になりますが、しばらくすればここらへんは意識しなくても良くなるはずです。
- Vue:3.2.26
- Jest:27.5.1
前提:Reactivity Transformとは
Vue 3.2.25からExperimentalとして追加されたコンパイラーマクロで、Vueが提供しているReactiveな値を.value
を経由せずに取得することができるようになります。
// 通常のref import { ref } from 'vue' const counter = ref(0) console.log(counter.value) // 0 // Reactivity Transform const counter = $ref(0) console.log(counter) // 0
公式ドキュメントでは以下の章で紹介されていて、refだけでなく、computedも$computedとして使用することができます。
問題:JestがReactivity Transformを解釈できない
今開発しているアプリはWebpackを利用しているので(正確にはWebpacker...殺せっ!)、ドキュメントの通り、明示的に設定に追記することでこの機能を有効化することができます。
module.exports = { test: /\.vue$/, loader: 'vue-loader', options: { reactivityTransform: true } }
これでアプリ実行時はうまくいくのですが、ユニットテストに使用しているJestではWebpackは関係ないため、この構文の解釈ができずにエラーになります。
node:internal/process/promises:265 triggerUncaughtException(err, true /* fromPromise */); ^ [UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "ReferenceError: $computed is not defined".] { code: 'ERR_UNHANDLED_REJECTION' }
今回はこれを解消します。
対処:@vue/vue3-jest@27.0.0-alpha.3以上を導入する
yarn add -D @vue/vue3-jest@27.0.0-alpha.4
もしくは@vue/vue3-jest@latest
を実行します。
2022/03/15時点ではalpha4が最新版なので、こう書いていますが27.0.0-alpha.3以上を導入すればOKです。
これによりテスト実行時のログに、webpackで実行したときと同じようにexperimentalに対するログが出るようになり、Reactivity Transformが正しく解釈されるようになります。
[@vue/ref-transform] Reactivity transform is an experimental feature. Experimental features may change behavior between patch versions. It is recommended to pin your vue dependencies to exact versions to avoid breakage. You can follow the proposal's status at https://github.com/vuejs/rfcs/discussions/369.
解説
Reactivity Transformのみならず、Vueの仕様についてはGitHub Discussionsにて議論されています。
この中でJestへの対応に関する質問がありました。
VueとAngularのコントリビュータであるCédricさんが「vue3-jest」に新しいオプションを加えたよという話をしています。それがこのPRです。
READMEにも同様の内容が書いてあるのですが、27.0.0-alpha3以降は以下のオプションでReactivity Transformを解釈できるようになります。
globals: { 'vue-jest': { compilerOptions: { refTransform: false } } }
デフォルトではこれがtrueだそうで、先に述べた通りalpha3以上を導入すれば自動的にReactivity Transformが解釈できるようになるわけです。
おわりに
これを解消したはいいんですけど、まだ私のJestはエラーを出力しています。やばいですねっ!
【Rails】collection_selectで表示するテキストには、Procを指定できる
事の発端
自分で作ったGem「jp_local_gov」を使って、自作サービスを実装しています。
実装にあたって、市区町村名のセレクトボックスが必要でした。
jp_local_gov
ではView Templateに記載の通り、collection_select
で使用する候補を返すことを目的の一つとしたJpLocalGov.all
というAPIを実装しているので、一見以下の形式で目的は達成できそうです。
f.collection_select :local_gov_code, JpLocalGov.all, :code, :city
たしかにこれでも動作としては問題ないのですが、city
としか指定していないので、「市区町村名」しか表示されません。
日本には「都道府県は異なるけど、市区町村名が同じ市区町村」がそこそこ存在します。
JpLocalGov.all.group_by(&:city).select { |k, v| v.size > 1 }.map { |k, v| { k => v.map(&:prefecture) }} => [{"伊達市"=>["北海道", "福島県"]}, {"松前町"=>["北海道", "愛媛県"]}, {"森町"=>["北海道", "静岡県"]}, ...
そのため、都道府県と市区町村を合わせてテキストに表示したかったので、雑に以下のようにやってみたのですが動かず。
f.collection_select :local_gov_code, JpLocalGov.all, :code, "#{prefecture}#{:city}"
collection_selectで表示するvalueとtextにはProcを指定できる
困った時のAPI Dockというわけでチェックしてみると、
The :value_method and :text_method parameters are methods to be called on each member of collection. The return values are used as the value attribute and contents of each
要はcollection_selectの第1引数に指定したオブジェクトが対応できるメソッドを呼び出すことができ、それゆえにProcも呼び出せるようです。
結果として、
f.collection_select :local_gov_code, JpLocalGov.all, :code, ->(lg) { "#{lg.prefecture} #{lg.city}" }
こんな感じにProcを渡してあげると、JpLocaGov.all
で取得されたすべての市区町村がイテレーションされてlg
にわたり、それぞれの「都道府県名 市区町村名」として表示できるわけです。
Formオブジェクトのロケールファイルの定義方法(i18n)
Formオブジェクトを利用した場合の翻訳キーはどれ?
通常DBに紐づくモデル、つまりActiveRecord::Base
を継承したクラスに対応するフォームを作成する場合、次のようなロケールファイルを作成することで、バリデーションメッセージや表示される項目を多言語化することができます。
ja: activerecord: # activerecordをキーにする attributes: user: name: 名前 password: パスワード
一方DBへの保存だけではなく、複雑なユースケースを実装する場合にはFormオブジェクトを作って対処します。例えばフォームから入力されたデータを用いて、メール送信を行うような場合です。
Formオブジェクトは複数のモデルを扱ったり、DB以外の操作が伴うため、基本的にはActiveModel::Model
をincludeした形で実装します。
また、form_withからの送信先判定を行うために、to_modelをオーバーライドし、メインとなるModelクラスに向けることもあります。
...こうなってくると、i18n用のロケールファイルをどうやって書いたらいいのかよくわからなくなってきたので、Railsがどのようにロケールファイルを解釈しているのか調べました。
要約
- ActiveModelとActiveRecordはそれぞれ
i18n_scope
を実装していて、これによりロケールファイルのトップレベルの探索キーが決まる。 - Formオブジェクト側で行っている処理(例:バリデーション)は
activemodel
をキーにする - to_modelをオーバーライドしてActiveRecord::Baseを継承したモデルを参照している場合、それらの表示に対する翻訳は
activerecord
をキーにする
前提
対象バージョン
- ActiveRecord:~> 7.0.0
- ActiveModel:~> 7.0.0
サンプルケース
モデル:User
class User < ApplicationRecord end
フォームオブジェクト:UserForm
class UserForm include ActiveModel::Model include ActiveModel::Attributes attr_reader :user def initialize(user = User.new, **attributes) @user = user attributes = default_attributes if attributes.empty? super(attributes) end validates :name, presence: true # バリデーションがいっぱい attribute :name, :string # カラム定義も様々 def save # 実際の処理 end def to_model user end end
RailsがModelのi18nを探索する手順
バリデーションメッセージ
バリデーション用のエラーメッセージの組み立ては、ActiveModel::Error.generata_message
で行われています。
https://github.com/rails/rails/blob/main/activemodel/lib/active_model/error.rb#L64
def self.generate_message(attribute, type, base, options) # :nodoc: type = options.delete(:message) if options[:message].is_a?(Symbol) value = (attribute != :base ? base.read_attribute_for_validation(attribute) : nil) options = { model: base.model_name.human, attribute: base.class.human_attribute_name(attribute, { base: base }), value: value, object: base }.merge!(options) if base.class.respond_to?(:i18n_scope) i18n_scope = base.class.i18n_scope.to_s attribute = attribute.to_s.remove(/\[\d+\]/) defaults = base.class.lookup_ancestors.flat_map do |klass| [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}", :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ] end defaults << :"#{i18n_scope}.errors.messages.#{type}" catch(:exception) do translation = I18n.translate(defaults.first, **options.merge(default: defaults.drop(1), throw: true)) return translation unless translation.nil? end unless options[:message] else defaults = [] end defaults << :"errors.attributes.#{attribute}.#{type}" defaults << :"errors.messages.#{type}" key = defaults.shift defaults = options.delete(:message) if options[:message] options[:default] = defaults I18n.translate(key, **options) end
https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-generate_message
抜粋するとこんな感じです。
def self.generate_message(attribute, type, base, options) # :nodoc: # 1: 対象のクラスについて、i18n_scopeを実行し、キーを取得する。 if base.class.respond_to?(:i18n_scope) i18n_scope = base.class.i18n_scope.to_s attribute = attribute.to_s.remove(/\[\d+\]/) defaults = base.class.lookup_ancestors.flat_map do |klass| [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}", :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ] end # 省略 end # 2. 取得できた情報でデータを翻訳文を作成し、つっこむ key = defaults.shift defaults = options.delete(:message) if options[:message] options[:default] = defaults # 3. 2で作成されたデータで翻訳処理を行う(ロケールに応じたファイルを選択する) I18n.translate(key, **options) end
ここで肝になるのはi18n_scope
というメソッドです。このメソッドにより、activerecord
を見にいくのか、activemodel
を見にいくのかが決まります。
ではその実装はどうなっているのかというと、ActiveModelの場合、次のようになっています。
# Returns the +i18n_scope+ for the class. Override if you want custom lookup. def i18n_scope :activemodel end
一方で、ActiveRecordでも同じメソッドがオーバーライドされており、次のように実装されています。
# Set the i18n scope to override ActiveModel. def i18n_scope # :nodoc: :activerecord end
これにより、翻訳対象のクラスが
- ActiveModelの場合は、
activemodel
- ActiveRecordの場合は、
activerecord
が翻訳キーとして採用されることになります。
ここで、バリデーションメッセージについては、Formオブジェクト、つまりActiveModelで実装されたものです。そのため、エラーメッセージの翻訳キーはactivemodel
始まりになります。
ja: activemodel: user_form: name: 名前 password: パスワード
カラム(画面表示項目)
カラム(画面表示項目)の翻訳についても先のi18n_scopeで説明がつきます。
form_withでフォームを作成する際に、to_modelによって、ActiveRecordであるUser側にクラス判定が向くため、ActiveRecordのi18n_scopeが採用されます。
そのため、以下のようなロケールファイルを記述します。
ja: activerecord: user: name: 名前 password: パスワード
なおUserモデルに含まれない項目をFormオブジェクトに定義した場合も、翻訳ファイルはactiverecord下に記述することに注意が必要です。
以上Formオブジェクトでの翻訳キーの参照先についての調査でした。
Slapdashを使って、瞬間で Notionにアイデアを追加する
Notionは情報を貯めるには便利だけど、開くのは面倒
Notionも「辛い」と言われるくらいに普及してきましたね。英語版だけだったり、無料ユーザーはブロック数に制限があったりした時代が懐かしいです。
やっぱり1つの情報を複数の表示形式で閲覧することができるのは非常に画期的で、自分もブログネタを管理するのに使用しています(同じDBをボードとカレンダーで表示して、コンテンツの状況と締切を一覧している)。
こうやって溜まった情報を閲覧するにはすごい便利なんですが、問題はちょっとリッチ & ゆるすぎて、新しい情報を登録するためにわざわざNotionを開くのが面倒くさいんですよね。
特にブログ記事みたいなアイデアは、他の作業をしている時にふっと思いつくものなので、Notionを開くことによるコンテキストスイッチが発生するのがネックでした。
そこでSlapdashというコマンドランチャーの機能を使い、Notionへの登録を簡略化したのですが、これが結構よかったのでご紹介したいと思います。
Slapdashの使い方
Slapdashとは
Slapdashは各種Webアプリを統合し、まとめて操作できるランチャーアプリです。
TrelloやAsanaといったプロジェクト管理ツールから、GitHub、Figmaといったクリエイティブツールまで、このアプリから一括で検索、追加、編集を行うことができます(※:アプリによって、操作できる範囲は異なります)。
例えばGitHubを連携した場合、GIFのようにどこからでもコマンドパレットを立ち上げ、自分が関与しているリポジトリのIssueを横断して検索することができます(ここからIssueを選択し、登録・編集操作が可能です)。
価格についてはBasic、Pro、Teamsの3つのプランがありますが、今回紹介する範囲はBasic、すなわち無料で使える範囲での機能紹介になります(私もBasic会員です)。
ランチャー画面とは別に起動画面があり、ここからアプリを連携したり、Spaceと呼ばれる空間にリンクを突っ込んだり、自分専用のコマンドを作成することができます。
Notionと連携する
SlapdashはNotionにも対応しており、操作としては
- ページの新規作成(Not DB)
- DBに新たにページを追加
- 既存のページに対してコンテンツを追加
の3つに対応しています。
Notionを連携する場合は、Slapdashを起動してから「Apps」を選択し、
「App Store」からNotionを選択します(私の場合は接続済になっているので表示が異なりますが、新規接続の場合はConnectというボタンが表示されているので、それを選択します)。
この接続を行った時点で存在するNotionのページやDBに対して、Slapdashへと編集の許可が降りるようになっています。そのため、接続後に新しくNotionページやDBを追加し、それらに対してSlapdashから操作を行いたい場合は、Notion側でAPIを許容する必要があります(Notion右上のShare → SlapdashをInviteする)。
Slapdashから実行できるNotionの操作
Notion内の検索を行う
「Notion」と入力し、「Filter by Notion」を選択することで、Notionのページを横断的に検索することができます。また「Notion」と入力しなくても検索ワードを直接入力してもページやDBにたどり着くことができます。
SlapdashからNotionのページを作成する
「Notion」→「Create New Notion Page」と進むことで、単一のページを作成することができます。作成する際には作成場所となる親のページを指定します。
作成する際にページタイトルやコンテンツは指定できません。あくまでページを作成するのみです。
SlapdashからNotionのデータベースにページを追加する
「Notion」→「Add to Notion Database」と進み、対象のDBを指定することで、そのDBに新しくページを追加することができます。
紙芝居でイメージをご紹介しましょう🐵🦀
例えば私の場合だと、このようなプロパティを持ったブログネタ管理用のDBがあります。ここに新しくページを追加します。
対象のDBとして「ブログ」にカーソルを合わせTab or Enterキーを押します。
するとこのDBのプロパティとコンテンツを入力するためのフォームが出てきます。各プロパティには、Notionであらかじめ指定しておいたフォーマットに合わせて入力を行うことができます(リストやカレンダーなど)。
「Add」を押すと、作成した記事に対するアクションを指定することができます。今回は「Open in Notion」で作成した情報をチェックしてみましょう。
先程入力した内容がNotionに追加されました。Markdownも正しく反映されています。
追加したNotionのページにコンテンツを追加する
先ほどはNotionにページを追加する操作でしたが、追加したページに内容を追記することもできます。
「Notion」→「Add to Notion Page」と進み、ページを指定することでコンテンツを追加することができます。こちらも紙芝居で動作をみていきましょう。先程追加した「新しいブログ記事」に対して追加を行っていきます。
ページを選択すると、コンテンツの入力フォームが表示されます。こちらもMarkdownで内容を記述することができます。
内容を追記したら「Add」を選択し、先程と同じように「Open in Notion」で作成した情報をチェックしてみます。
ページの最下部に今追加し内容が追記されました!
Tips:templateを作成する
「このDBにテーブルを追加することがほとんど」「新規作成時はこのステータスで登録する」といった具合に、具体的なケースがある場合はSlapdashのテンプレート機能を利用して効率化することができます。
Slapdashから「Create Command」→「Template」と進みます。
テンプレートは連携しているサービスに対してそれぞれ作成できるのですが、今回はNotionへのDB追加のテンプレートを作成するため、「Add to Notion Database」を指定します。
まずはNotionのテンプレートを指定します。ここで指定した項目がSlapdashからの登録時のデフォルト値になります。
Notion側の設定を終えたら、ページをスクロールしテンプレート自体の設定を行います。
Nameにはテンプレートの名前を、その他アイコンや説明を追記します。エイリアスを設定しておくと、Slapdashのコマンドパレットにそのエイリアスを入力することで、テンプレートを呼び出すことができます。
設定が完了したら「Create Command」を押して、実際に試してみましょう!今回はwtというエイリアスを貼ったので、これをキーに呼び出してみます。
呼び出した入力画面に、あらかじめ設定した内容が反映されていることが確認できました。
おわりに
というわけでSlapdashを使ったNotionの更新フローの紹介でした。
私はこれを使って、冒頭にも述べた通りブログネタを登録して思いついた時にアイデアを追記したり、やりたいことリストを追加したりしています。
思いついた時に登録することでアイデアを漏らさず、かつどこからでも登録できることで現在のタスクから大きくコンテキストスイッチをすることもなく、快適に生活できています。
SlapdashはNotion以外のツールにも対応していますので、ぜひ試してみてください。
Appraisalを使っている状態でもRubyMineのGUIでRSpecを実行する
きっかけ
先日開発したGemはActiveRecordへの対応を仕様として盛り込んでいるため、Appraisalを使って、複数verのActiveRecordでテストができるようにしています。
それはそれでいいんですが、この状態でいつものようにRubyMine上からテストを実行しようとすると、Appraisalで指定しているはずのGem(ActiveRecord)がないためにテストが落ちるようになってしまいました(コマンドラインからAppraisal経由でテストをすれば問題なくとおる)。
Testing started at 7:13 ... An error occurred while loading spec_helper. - Did you mean? rspec ./spec/spec_helper.rb Failure/Error: require "active_record" LoadError: cannot load such file -- active_record # ./spec/spec_helper.rb:6:in `require' # ./spec/spec_helper.rb:6:in `<top (required)>' Run options: include {:full_description=>/JpLocalGov\.find/} All examples were filtered out
というわけで今回はAppraisalを経由しても、RubyMine上でテストを実行できるようにしていきます。
TL:DR;
- Apppraisalを導入する場合、各種コマンドは
bundle exec appraisal
の形式で呼び出す必要がある。 - RubyMine標準のRSpec実行は
bundle exec rspec ….
であるため、Appraisalが導入されていて、そこでインストールされたGemに依存する実装・テストを書いている場合、RubyMine経由でRSpecが実行できなくなる。 - AppraisalのCLIを経由するscriptを作成し、RubyMineに設定することで、RubyMine経由でもRSpecを個別に実行できるようになる。
前提のおさらい
Appraisal
Appraisalはthoutbot社が管理している、複数のバージョンでのテストを同時に行うためのGemです。
公式READMEからの拝借になりますが、以下のように指定することで、Railsの3と4の双方の環境でテストを実行することができます。通常のGemfileでは1つのGemに対して、1つのバージョンしか指定できないため、複数環境に対応したい場合に重宝します。
appraise "rails-3" do gem "rails", "3.2.14" end appraise "rails-4" do gem "rails", "4.0.0" end
Appraisalを使用する場合、各種コマンドは次のように実行することになります。
bundle exec appraisal {実行対象の環境} <実行したいコマンド> # 上記例で、rails-3のかんきょうでテストしたい場合、 bundle exec appraisal rails-3 rspec
RubyMineでのRSpec実行
RubyMineではテスティングフレームワークに関係なく、GUI上でテストの実行ができます。
一括実行と個別実行の双方を行うことができます。
内部的には、各テスティングフレームワークのexampleやname指定のオプションなどを付与しながら、bundle exec rspec
を実行しているだけです。
問題点
上記の通り、Apppraisalはbundle exec appraisal
の形式で各コマンドを呼び出すことで、指定した環境のGemfileでコマンドを実行することができます。
逆にいえばAppraisalを経由しないと、Appraisalで指定したGemfileは認識されません。冒頭に述べた通り、Appraisalは複数のバージョンでのテストを可能にするためのGemなので、Appraisalを経由しない = テストが実行できない ということです。
RubyMineでのテスト実行はAppraisalを経由しない形式になっているので、結果としてAppraisalを利用すると、テストが実行できないということになります。
対応策
以下の2ステップで対応します。
- Appraisal経由でテストを実行するscriptを作成する
- 1のscriptをRubyMineに認識させる。
scriptを作成する
require 'rubygems' require 'bundler/setup' require 'appraisal' require 'appraisal/cli' begin appraisal_name = ''rails-5.0" # this is just an example, use the appraisal that you have installed cmd = [appraisal_name, 'rspec'] + ARGV Appraisal::CLI.start(cmd) rescue Appraisal::AppraisalsNotFound => e puts e.message exit 127 end
AppraisalのIssueを確認していると、同じような問題が起票されていました。
AppraisalにCLIがあるので、それを間接的に呼び出すようです。このコードでは特定のAppraisal環境をしていするようになっていますが、とりあえずすべてのAppraisal環境で実行するように少し修正しました。
require "rubygems" require "bundler/setup" require "appraisal" require "appraisal/cli" begin `bundle exec appraisal list`.split(/\R/) do |appraisal_name| cmd = [appraisal_name, "rspec"] + ARGV Appraisal::CLI.start(cmd) end rescue Appraisal::AppraisalsNotFound => e puts e.message exit 127 end
RubyMineにscriptを認識させる
RubyMineのGUI実行は、それぞれ実行の構成を指定することができます。
複数の構成に対して、まとめて設定を行うためには「Edit configuration templates...」を選択します。
左側のサイドバーからRSpecを選択し、「Use cutom RSpec runner script」に、1で作成したscriptを設定します。scriptはどこにおいてもいいですが、Appraisalはプロジェクト固有のものだと思うので、私はプロジェクトのbinフォルダにおいています。
これにより、RubyMineがRSpecを実行するときは、このコマンドを実行するようになります。
実行結果
BUNDLE_GEMFILE=/Users/tadokoroikuma/RubymineProjects/jp_local_gov/gemfiles/rails61.gemfile bundle exec rspec
これらの設定でRSpec実行時にAppraisalのCLIが走るようになり、Gemfileを指定した状態でテストが実行できるようになりました。
感想
Appraisalで作成した環境に対してマトリックス実行する分にはこのスクリプトでいいんですが、「ある特定のテストを特定の環境でのみ実行したい」というケースは対応できないので、対応できるようにしておきたいです(スクリプトの引数処理→Appraisal CLIの引数処理の部分だけクリアできれば、ちゃちゃっとできると思うので)。
Conventional CommitでGemのCHANGELOGをコミットから自動作成する
Conventional Commitとは
概要
Gitのコミット粒度はまずConventional Commitsで種類が混ざりにくいようにしつつ、「小さいコミットを後からくっつけたり並べ替えたりする方が、大きいコミットを後から分割するよりもずっと簡単なので、迷ったら細かくコミットして後から見直せばいいよ」とよく言っていますhttps://t.co/ySrZSP2xQ7
— Takuto Wada (@t_wada) 2021年12月3日
t_wadaさんが以前Twitterで言及されているのを見かけて、Conventional Commitなるものがあることを知りました。
Conventional Commitとは、以下のようなコミットメッセージの規約を指します。
Conventional Commits の仕様はコミットメッセージのための軽量の規約です。 明示的なコミット履歴を作成するための簡単なルールを提供します。この規則に従うことで自動化ツールの導入を簡単にします。 コミットメッセージで機能追加・修正・破壊的変更などを説明することで、この規約は SemVerと協調動作します。
形式
Conventional Commitは以下の形式を取ります。
<型>[任意 スコープ]: <タイトル> [任意 本文] [任意 フッター]
型はfeat
やfix
、docs
など、変更を端的に表した単語です。具体的には次のようになります。
feat(subscribe): メール購読解約時のログインを不要にした ユーザーのストレス軽減のため(適当) Closes: #79
RubyMineでのサポート
にもある通り、Conventional Commitをサポートするためのツールはかなりたくさんあります。 所詮はコミットメッセージなので、人力で入力してもConventional Commitにはできるのですが、型やフッターを選択肢から選べたり、枠が決まっているので形式を意識せずにメッセージを作れたりするので、あった方が何かと便利です。
このページにもInteliJ用のツールは掲載されているのですが、私は別途「Conventional Commit」というツールを使っています(RubyMineのPluginストアで評価が高いのを選んだだけ)。
Conventional CommitでCHANGELOGを作る
で、ここからが今回の本題で、Conventional CommitからCHANGELOGを作成します。
何故-conventional-commits-を使うのか には、理由の1つに
• 変更履歴 (CHANGELOG) を自動的に生成できます。
とあって、どうやって自動生成できるのかなと思ったんですが、コミットメッセージが規約に則っていることで、別途ツールを使用すれば機械的にCHANGELOGが作成できるということのようです。
今回は以前リリースしたGemのCHANGELOGを作成してみることにしました。
conventional-changelog
Conventional Commitから色々やるためのツールが、conventional-changelogとしてまとまっています。
この中のconventional-changelog-cliを使用してCHANGELOGを作成します。
インストール
conventional-changelog-cliはnpmなので、まずはインストールします。
リリースのためだけにリポジトリを汚したくないので、グローバルインストールします(グローバルを汚すのはいいんかい)。
$ npm i -g conventional-changelog-cli
コマンド
conventional-changelog
コマンドで実行します。helpを見ると分かる通り、結構色々なオプションがあります。
$ conventional-changelog --help Generate a changelog from git metadata Usage conventional-changelog Example conventional-changelog -i CHANGELOG.md --same-file Options -i, --infile Read the CHANGELOG from this file -o, --outfile Write the CHANGELOG to this file If unspecified, it prints to stdout -s, --same-file Outputting to the infile so you don't need to specify the same file as outfile -p, --preset Name of the preset you want to use. Must be one of the following: angular, atom, codemirror, conventionalcommits, ember, eslint, express, jquery or jshint -k, --pkg A filepath of where your package.json is located Default is the closest package.json from cwd -a, --append Should the newer release be appended to the older release Default: false -r, --release-count How many releases to be generated from the latest If 0, the whole changelog will be regenerated and the outfile will be overwritten Default: 1 --skip-unstable If given, unstable tags will be skipped, e.g., x.x.x-alpha.1, x.x.x-rc.2 -u, --output-unreleased Output unreleased changelog -v, --verbose Verbose output. Use this for debugging Default: false -n, --config A filepath of your config script Example of a config script: https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-cli/test/fixtures/config.js -c, --context A filepath of a json that is used to define template variables -l, --lerna-package Generate a changelog for a specific lerna package (:pkg-name@1.0.0) -t, --tag-prefix Tag prefix to consider when reading the tags --commit-path Generate a changelog scoped to a specific directory
実際に作ってみる
conventional-changelog -p eslint -i CHANGELOG.md -s
今回はESLint形式で、CHANGELOG.mdに対して追記をしていきます(なんでESLintにしたかというと、ESLintが一番よく知っているパッケージだからというだけです、すみません😅)。
このコマンドを実行してできたCHANGELOGがコチラになります。
# [](https://github.com/IkumaTadokoro/jp_local_gov/compare/v0.1.0...v0.) (2022-01-14) ### chore * Add RSpec Runner for JetBrains IDE ([83d8c86](https://github.com/IkumaTadokoro/jp_local_gov/commit/83d8c8649f69506c9a08acf275960480316eb988)) * Make JpLocalGov::Data::Importer#prefecture_capital? to return true/false ([82daafd](https://github.com/IkumaTadokoro/jp_local_gov/commit/82daafd19b47e52ff2c1c341a243aef7e5dffae4)) ### ci * Add spell checking GitHub Actions Workflow ([fc23bd1](https://github.com/IkumaTadokoro/jp_local_gov/commit/fc23bd1a3c10cc49f8aaf4e869c0062b50255f03)) ### docs * 💅 ([61c6d66](https://github.com/IkumaTadokoro/jp_local_gov/commit/61c6d66a43b75339036656a1b110dec97156177e)) * Add how to use JpLocalGov.valid_code? ([2e14d71](https://github.com/IkumaTadokoro/jp_local_gov/commit/2e14d71c146d823e4bc3fa268da2e3a0c5f7ebe1)) * Add issue template ([9f416a3](https://github.com/IkumaTadokoro/jp_local_gov/commit/9f416a335247c3be5c52e450b914bd73c380092d)), closes [#47](https://github.com/IkumaTadokoro/jp_local_gov/issues/47)
Closing Keyword を使ってIssueをクローズしていると、参照先のIssueも表示されるみたいです。これは結構便利ですね。
PRをもらった場合にどうするか?
そのリポジトリにコミットする人がみんなConventional Commitを使うかというとOSSの場合はそうもいかないと思います。
私も実際にそのケースに出くわしたのですが、「今回は特例で手動でCHANGELOGを追加するか」ということで、特に調べず終わってしまいました。
で、この記事を執筆する過程で調べてみたところ、以下の記述が見つかりました。
いいえ! Git で squash ベースのワークフローを使用する場合は、主要メンテナがマージ時にコミットメッセージをクリーンアップすることができるため、臨時のコミッタには作業負荷がかかりません。 また、これをするための一般的な方法としては、プルリクエストからのコミットを git システムが自動的に squash し、主要メンテナによるマージ時に適切な git コミットメッセージを入力するためのフォームを表示するというものです。
Conventional Commit側の主張は、「メンテナが都度直せばいいじゃん」とのことでした...。他のリポジトリとかどうしているんだろう?それこそRailsのような大きなリポジトリだとなかなか難しいと思うんですが。
感想
というわけでConventional Commitで自動的にCHANGELOGを作る方法でした。
個人的にはコマンド1つでCHANGELOGが完成するので、気楽にupdate作業をできていい感じです。
PRのマージコミットメッセージの件も含めて、他のOSSがどんな感じにCHANGELOGを書いているのかがすごい気になったのですが、残念ながら作成する過程は見えないので、地域rbとかで聞けたら聞いてみようと思います。
余談:もう少しコミットは細かく積もうかな
少しずれるのですが、最近ちょっと前に以下の動画を見た影響もあって、「できるだけコミットはsquashしておこう」と強く意識しすぎていました。
そうすると、なかなかコミットが打てなくて作業的には辛いので、
- 作業時はconventional commitでこまめにコミットを打つ
- Pushするときにsquashする
みたいなフローにしたほうがいいなと記事を書いていて思いました。