- 追記
- これは
- tl;dr
- 何にハマったのか
- 何がどうだったのか
— octorelease が依存しているライブラリ
— デバッグしてみる
— デバッグしてみる (2) 〜 何が起きているのか 〜
— デバッグしてみる (3) 〜 じゃあ, どうするのか 〜 - で, どうしたのか
— まぼろしのプルリクエスト
— なぜ, オレオレ YAML パーサーなのか - ということで
追記
プルリクエストは静かにクローズされました. 丁寧にコメント頂いて嬉しかったです.
github.com
これは
qiita.com
初老丸 Advent Calendar 2018 第 1 日目の記事になる予定です.
tl;dr
octorelease という Gem をリリースする際に過去のプルリクエストを git log から拾ってリリースノートを自動生成するツールを利用しようとしたら, それに依存する hub でハマってしまったのでメモしておきます.
github.com
github.com
何にハマったのか
octorelease (厳密に言うと, hub) を利用するにあたって, 事前に Github API を利用する為に ${HOME}/.config/hub
に以下のような設定を YAML で設定しておきます.
--- github.com: - user: xxxxxx oauth_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
この YAML の書き方が実は問題だったのです… (後述)
rake octorelease
を実行すると以下のような例外が発生しました.
rake aborted! NoMethodError: undefined method `[]=' for nil:NilClass /path/to/vendor/bundle/ruby/2.5.0/gems/hub-1.12.4/lib/hub/github_api.rb:511:in `block in yaml_load' /path/to/vendor/bundle/ruby/2.5.0/gems/hub-1.12.4/lib/hub/github_api.rb:502:in `each' /path/to/vendor/bundle/ruby/2.5.0/gems/hub-1.12.4/lib/hub/github_api.rb:502:in `yaml_load' /path/to/vendor/bundle/ruby/2.5.0/gems/hub-1.12.4/lib/hub/github_api.rb:483:in `load' /path/to/vendor/bundle/ruby/2.5.0/gems/hub-1.12.4/lib/hub/github_api.rb:456:in `initialize' /path/to/vendor/bundle/ruby/2.5.0/gems/octorelease-0.0.6/lib/octorelease.rb:9:in `new' /path/to/vendor/bundle/ruby/2.5.0/gems/octorelease-0.0.6/lib/octorelease.rb:9:in `block in <top (required)>' ... Tasks: TOP => octorelease
例外メッセージを見る限りだと, octorelease が依存している hub というライブラリで問題が発生しているようです.
何がどうだったのか
octorelease が依存しているライブラリ
hub というコマンドラインツールで, ライブラリとしても利用が可能なようです. あまり詳しく見ていないので, 間違っていたら指摘をお願い致します.
github.com
ちなみに, 現在は Golang で書き直されているようです.
デバッグしてみる
例外のメッセージを見てみると, hub の github_api.rb というコードの 511 行目で何か起きているようです. 以下, 前後のコードの抜粋です.
... def yaml_load(string) hash = {} host = nil string.split("\n").each do |line| case line when /^---\s*$/, /^\s*(?:#|$)/ # ignore when /^(.+):\s*$/ host = hash[$1] = [] when /^([- ]) (.+?): (.+)/ key, value = $2, $3 host << {} if $1 == '-' host.last[key] = value.gsub(/^'|'$/, '') else raise "unsupported YAML line: #{line}" end end hash end ..
メソッド名を見ると, YAML ファイル (${HOME}/,config/hub) を読み込んで解析するようなメソッドのようですので, irb を起動してこのメソッドをデバッグしてみたいと思います. 用意する YAML ファイルは, 以下のような内容となります.
github.com: - user: foobar oauth_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
試しに Ruby 標準の YAML ライブラリを利用して解析してみます. 尚, 検証に利用する Ruby の環境は以下の通りです.
$ ruby --version ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17
以下のように, Ruby 標準 YAML ライブラリの load_file メソッドを利用してファイルから YAML を読み込みます.
irb(main):001:0> require 'yaml' => true irb(main):003:0> YAML.load_file('sample.yml') => {"github.com"=>[{"user"=>"foobar", "oauth_token"=>"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}]}
ちゃんと解析されて Ruby のハッシュオブジェクトとして読み込まれました. 次に hub で YAML を読み込んでみます. octrelease のソースコードを見る限りだと, hub の Hub::GitHubAPI::FileStore というクラスをインスタンス化する際に YAML ファイルを引数として渡してあげると良さそうです.
irb(main):004:0> require 'hub' => true irb(main):006:0> Hub::GitHubAPI::FileStore.new 'sample.yml' Traceback (most recent call last): ... 5: from /path/to/vendor/bundle/ruby/2.5.0/gems/hub-1.12.4/lib/hub/github_api.rb:456:in `initialize' 4: from /path/to/vendor/bundle/ruby/2.5.0/gems/hub-1.12.4/lib/hub/github_api.rb:483:in `load' 3: from /path/to/vendor/bundle/ruby/2.5.0/gems/hub-1.12.4/lib/hub/github_api.rb:502:in `yaml_load' 2: from /path/to/vendor/bundle/ruby/2.5.0/gems/hub-1.12.4/lib/hub/github_api.rb:502:in `each' 1: from /path/to/vendor/bundle/ruby/2.5.0/gems/hub-1.12.4/lib/hub/github_api.rb:511:in `block in yaml_load' NoMethodError (undefined method `[]=' for nil:NilClass)
冒頭の例外と同じ内容の例外が発生しました. この例外を回避する為, 試行錯誤した結果, 以下のような YAML ファイルの中身にすることで例外を回避することを確認しました. (github.com
: 以下に半角スペースのインデントが無い状態です.)
github.com: - user: xxxxxx oauth_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
この YAML を一行にすると以下のようになります.
---\ngithub.com:\n- user: foobar\n oauth_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n
これを, 標準の YAML ライブラリで読み込んでみます.
irb(main):001:0> require 'yaml' => true irb(main):002:0> YAML.load("---\ngithub.com:\n- user: foobar\n oauth_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n") => {"github.com"=>[{"user"=>"foobar", "oauth_key"=>"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}]}
一応, YAML として読み込むことが出来るようですが, こちら をざっくりと読んでみたところ, YAML 自体は半角スペースによるインデントを使って構造化しているので, 半角スペースでインデントは必須なんではなかろうかと考えていますが, github.com:
以下に半角スペースのインデントが無い状態でも問題は無いようです.
デバッグしてみる (2) 〜 何が起きているのか 〜
インデントが無い YAML でも問題は無かったのですが, 少々納得がいかなかったので, 引き続きデバッグを進めてみたいと思います.
hub の github_api.rb, 511 行目前後にフォーカスしてみます.
... when /^([- ]) (.+?): (.+)/ key, value = $2, $3 host << {} if $1 == '-' host.last[key] = value.gsub(/^'|'$/, '') ...
文字列として読み込まれた YAML を 1 行毎に正規表現を使ってキャプチャして特殊変数の $1
〜 $3
に放り込んでいるようです. そして, $1
が -
であれば, host
という変数に空の{}
(ハッシュ) を追加して, $2
をハッシュのキー (変数 key
) として, $3
を key
に対する値 (変数 value
) としてハッシュを生成していくことを意図しているようです.
では, このコードを含む yaml_load メソッドだけを切り出して, 以下のような小さなコードでデバッグしてみたいと思います. デバッグには byebug という Gem で配布しているデバッガを利用します.
require 'minitest/autorun' class HubYamlLoadTest < Minitest::Test def test_yaml_load_my_pattern yaml = "---\ngithub.com:\n - user: foobar\n oauth_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n" expect = {"github.com"=>[{"user"=>"foobar", "oauth_key"=>"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}]} assert_equal yaml_load(yaml), expect end end def yaml_load(string) hash = {} host = nil string.split("\n").each do |line| case line when /^---\s*$/, /^\s*(?:#|$)/ # ignore when /^(.+):\s*$/ host = hash[$1] = [] when /(^[- ]) (.+?): (.+)/ key, value = $2, $3 host << {} if $1 == '-' require 'byebug'; byebug # ここにデバッガを差し込む host.last[key] = value.gsub(/^'|'$/, '') else raise "unsupported YAML line: #{line}" end end hash end
byebug の詳しい使い方については, 他の書籍やサイトをご覧下さい.
このテストコードを実行してみます.
$ bundle exec ruby test.rb Run options: --seed 54106 # Running: [25, 34] in /Users/kawahara/sandboxies/octorelease/test.rb 25: host = hash[$1] = [] 26: when /(^[- ]) (.+?): (.+)/ 27: key, value = $2, $3 28: host << {} if $1 == '-' 29: require 'byebug'; byebug => 30: host.last[key] = value.gsub(/^'|'$/, '') 31: else 32: raise "unsupported YAML line: #{line}" 33: end 34: end (byebug)
byebug のプロンプトが現れ, デバッガを差し込んだ次の行で処理が一時停止しています. この状態で, 各変数にどのような値が格納されているのか確認しています.
... (byebug) key "- user" (byebug) value "foobar" (byebug) host []
変数 host
が []
と空の配列になっており, 変数 host
はハッシュである前提で host.last[key]
が実行されるので, キーに変数 key
が代入することが出来ずに例外が発生してしまっているようです.
デバッグしてみる (3) 〜 じゃあ, どうするのか 〜
以下のような変更を加えてみます.
$ diff -u test.rb fix.rb --- test.rb 2018-11-22 07:09:51.000000000 +0900 +++ fix.rb 2018-11-22 07:09:42.000000000 +0900 @@ -25,7 +25,8 @@ host = hash[$1] = [] when /(^[- ]) (.+?): (.+)/ key, value = $2, $3 - host << {} if $1 == '-' + host << {} if $1 == '-' or $2 =~ /^\s*-\s*/ + key.gsub!(/^\s*-\s*|^\s*/, '') require 'byebug'; byebug host.last[key] = value.gsub(/^'|'$/, '') else
この状態で, 変更したコード (fix.rb) を実行してみます.
$ bundle exec ruby fix.rb Run options: --seed 49205 # Running: [26, 35] in /Users/kawahara/sandboxies/octorelease/fix.rb 26: when /(^[- ]) (.+?): (.+)/ 27: key, value = $2, $3 28: host << {} if $1 == '-' or $2 =~ /^\s*-\s*/ 29: key.gsub!(/^\s*-\s*|^\s*/, '') 30: require 'byebug'; byebug => 31: host.last[key] = value.gsub(/^'|'$/, '') 32: else 33: raise "unsupported YAML line: #{line}" 34: end 35: end (byebug) key "user" (byebug) value "foobar" (byebug) host [{}]
変数 host
に空のハッシュ {}
が格納されていることを確認しました. では, デバッガを外して実行してみます.
$ bundle exec ruby fix.rb Run options: --seed 17705 # Running: . Finished in 0.001235s, 809.7167 runs/s, 809.7167 assertions/s. 1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
いい感じですね. また, github.com:
以下に半角スペースのインデントが無い YAML でも正しく解析されるかも検証しておきたいので, 以下のようにテストを書きました.
require 'minitest/autorun' class HubYamlLoadTest < Minitest::Test def test_yaml_load_ok_pattern yaml = "---\ngithub.com:\n- user: foobar\n oauth_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n" expect = {"github.com"=>[{"user"=>"foobar", "oauth_key"=>"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}]} assert_equal yaml_load(yaml), expect end def test_yaml_load_my_pattern yaml = "---\ngithub.com:\n - user: foobar\n oauth_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n" expect = {"github.com"=>[{"user"=>"foobar", "oauth_key"=>"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}]} assert_equal yaml_load(yaml), expect end end def yaml_load(string) hash = {} host = nil string.split("\n").each do |line| case line when /^---\s*$/, /^\s*(?:#|$)/ # ignore when /^(.+):\s*$/ host = hash[$1] = [] when /(^[- ]) (.+?): (.+)/ key, value = $2, $3 host << {} if $1 == '-' or $2 =~ /^\s*-\s*/ key.gsub!(/^\s*-\s*|^\s*/, '') # require 'byebug'; byebug host.last[key] = value.gsub(/^'|'$/, '') else raise "unsupported YAML line: #{line}" end end hash end
これを実行してみます.
$ bundle exec ruby fix.rb Run options: --seed 30867 # Running: .. Finished in 0.003929s, 509.0354 runs/s, 509.0354 assertions/s. 2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
いい感じです. 2 つの YAML パターンが, この yaml_load メソッドで解析出来るようになったはずです.
で, どうしたのか
まぼろしのプルリクエスト
hub のリポジトリを見ると, Ruby で実装された hub は既にサポートされていないのではと思ってしまう程, master ブランチには Golang のコードが並んでいます. ブランチを github:1.12-stable を切り替えると様子が一変して Ruby で実装された hub を拝むことが出来ます. さらに, .travis.yml を見ると, 以下のようにテスト対象にはいにしへの Ruby バージョンが並んでいます.
sudo: false language: ruby before_install: - script/bootstrap - export PATH=~/bin:"$PATH" script: script/test bundler_args: --without development --deployment --jobs=3 --retry=3 cache: bundler rvm: - 1.8.7 - 1.9.2 - 1.9.3 - 2.0.0 - 2.1.5 - 2.2.0-preview2 notifications: email: false
どうやら, Ruby 版の hub はメンテナンスされていないようです. また, #1591 を見ろということでリンクが張っていますが, このリンクを踏んでも 404 となりページが存在していません. このページが無いことすらメンテナンスされていないということは本当にメンテナンスされていないと思って間違いないようです.
ですが, 一応, 以下のようなプルリクエストを作成してみました.
github.com
きっとマージされること無く, ひっそりとクローズされることでしょう.
なぜ, オレオレ YAML パーサーなのか
既にサポートされていないであろう Ruby 版 hub の .travis.yml を見ながら考えました. そもそも Ruby には YAML を読み込んだり, 書き出したりするライブラリがあるのに, なぜ hub にはオレオレ YAML パーサーが実装されているのか. 答えのようなコミットを発見しました.
github.com
2013 年の 12 月くらいまでは標準の YAML パーサーを利用していたようですが, YAML を解析する速度が思ったよりも遅かった為, オレオレ YAML パーサーに書き換えたとのことでした.
なるほどです. 実際に標準の YAML とオレオレパーサーでどのくらいの速度の違いがあるかについては別の機会に検証してみたいと思います.
ということで
octorelease から始まって, それが依存する hub のソースコードを見ながら色々と学びがありました. きっとプルリクエストはマージされずにまぼろしとなってしまうとは思いますが, これを糧に引き続き Ruby 道に精進して参る所存です.
有難うございました.