hotoolong's blog

プログラムのことやエンジニアリングに関することを記事にしています。

fzfをつかってRailsのmigrationを実行しやすくする

イントロ

Railsのmigationを実行する機会はそこまで多くはないと思うのですが、
generateしてつくったあとにVERSIONの選択が意外と面倒だったりします。

以前の記事

hotoolong.hatenablog.com

でgit statusをfzfのpreviewやbindを使って操作しやすくしましたが、

railsのmigration管理もpreviewとbindと親和性が高いなと思い作ってみました。

fish shell の function

以下のようなfish の function を作成しています。

function migrate
  if test ! -d 'db/migrate'
    echo 'Go to Rails Root'
    return
  end

  set -l out (ls -1 db/migrate | grep -v -e '^\.' | \
    fzf --exit-0 \
      --preview="bat --color=always db/migrate/{}" \
      --expect=ctrl-u,ctrl-d,ctrl-r,ctrl-m \
      --header='C-u: up, C-d: down, C-r: redo, C-m(Enter): edit' \
  )
  [ $status != 0 ]; and commandline -f repaint; and return

  if string length -q -- $out
    set -l key $out[1]
    set -l time (echo $out[2] | awk -F '_' '{ print $1 }')
    echo $key
    if test $key = 'ctrl-u'
      commandline "./bin/rails db:migrate:up VERSION=$time"
    else if test $key = 'ctrl-d'
      commandline "./bin/rails db:migrate:down VERSION=$time"
    else if test $key = 'ctrl-r'
      commandline "./bin/rails db:migrate:redo VERSION=$time"
    else if test $key = 'ctrl-m'
      commandline "$EDITOR db/migrate/$out[2]"
    end
    commandline -f execute
  end
end

操作方法

functionを登録してコマンドを実行するとfzf が起動します。

$ migrate

f:id:hotoolong:20200420002914g:plain
migration

migrate:up migrate:down migrate:redo を key bind しているので
バージョンを指定してアクションを決めて実行することができます。

これで少し便利になりました。

previewにはbatコマンドを使っていますが、headなどでもいいかと思います。

よかったら使ってみてください。

brewのformulaアップグレード時の依存関係のアップグレードによる弊害に対応する

イントロ

googlerをアップグレードしたところopensslのバージョンがあがって
他のツールが壊れてしまったのを戻す作業をした履歴です。
困ってる人の役に当てればと思います。

エラーと調査

ことの始まりは

$ brew upgrade googler

googlerの新しいバージョンがでてるからupgradeしました。

==> Upgrading 1 outdated package:
googler 3.7.1 -> 4.0_1
==> Upgrading googler 3.7.1 -> 4.0_1
==> Installing dependencies for googler: openssl@1.1, xz and python@3.8
==> Installing googler dependency: openssl@1.1
==> Downloading https://homebrew.bintray.com/bottles/openssl@1.1-1.1.1f.mojave.bottle.tar.gz
==> Downloading from https://akamai.bintray.com/25/25ab844d2f14fc85c7f52958b4b89bdd2965bbd9c557445829eff6473f238744?__gda__=exp=1586836802~hmac=b9c5e50d09
6a9d953daac8c75
######################################################################## 100.0%
==> Pouring openssl@1.1-1.1.1f.mojave.bottle.tar.gz
==> Caveats
A CA file has been bootstrapped using certificates from the system
keychain. To add additional certificates, place .pem files in
  /usr/local/etc/openssl@1.1/certs

and run
  /usr/local/opt/openssl@1.1/bin/c_rehash

openssl@1.1 is keg-only, which means it was not symlinked into /usr/local,
because macOS provides LibreSSL.

If you need to have openssl@1.1 first in your PATH run:
  echo 'set -g fish_user_paths "/usr/local/opt/openssl@1.1/bin" $fish_user_paths' >> ~/.config/fish/config.fish

For compilers to find openssl@1.1 you may need to set:
  set -gx LDFLAGS "-L/usr/local/opt/openssl@1.1/lib"
  set -gx CPPFLAGS "-I/usr/local/opt/openssl@1.1/include"

For pkg-config to find openssl@1.1 you may need to set:
  set -gx PKG_CONFIG_PATH "/usr/local/opt/openssl@1.1/lib/pkgconfig"

==> Summary
🍺  /usr/local/Cellar/openssl@1.1/1.1.1f: 8,057 files, 18MB
==> Installing googler dependency: xz
==> Downloading https://homebrew.bintray.com/bottles/xz-5.2.5.mojave.bottle.tar.gz
==> Downloading from https://akamai.bintray.com/44/44483961b5d2b535b0ece1936c9d40b4bc7d9c7281646cca0fb476291ab9d4dc?__gda__=exp=1586836818~hmac=45f8f52462
3d23ee5d13f2dad
######################################################################## 100.0%
==> Pouring xz-5.2.5.mojave.bottle.tar.gz
🍺  /usr/local/Cellar/xz/5.2.5: 92 files, 1.1MB
==> Installing googler dependency: python@3.8
==> Downloading https://homebrew.bintray.com/bottles/python@3.8-3.8.2.mojave.bottle.tar.gz
==> Downloading from https://akamai.bintray.com/51/511b4f2c3993f000516938ed0700936c8a7d8c054b5171fa733ac7d344291c30?__gda__=exp=1586836821~hmac=0271b874c6
5db4ee2c7f66f1d
######################################################################## 100.0%
==> Pouring python@3.8-3.8.2.mojave.bottle.tar.gz
==> /usr/local/Cellar/python@3.8/3.8.2/bin/python3 -s setup.py --no-user-cfg install --force --verbose --install-scripts=/usr/local/Cellar/python@3.8/3.8.
2/bin --install
==> /usr/local/Cellar/python@3.8/3.8.2/bin/python3 -s setup.py --no-user-cfg install --force --verbose --install-scripts=/usr/local/Cellar/python@3.8/3.8.
2/bin --install
==> /usr/local/Cellar/python@3.8/3.8.2/bin/python3 -s setup.py --no-user-cfg install --force --verbose --install-scripts=/usr/local/Cellar/python@3.8/3.8.
2/bin --install
==> Caveats
Python has been installed as
  /usr/local/opt/python@3.8/bin/python3

You can install Python packages with
  /usr/local/opt/python@3.8/bin/pip3 install <package>
They will install into the site-package directory
  /usr/local/opt/python@3.8/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages

brew upgrade すると
dependencyになっている formula がアップグレードされます。
openssl も バージョンアップされてしまいます。
以前もこの問題で python がアップグレードしてパスが変更されてしまいいろいろ動作できないソフトがあって困ったことがありました。

今回も気づかず
暫くして
rails cをしてみたところ以下のようなエラーに

$ rails c 
/rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `requir
e': dlopen(/rails_project/vendor/bundle/ruby/2.7.0/gems/mysql2-0.5.3/lib/mysql2/mysql2.bundle, 9): Library not loaded: /usr/local
/opt/openssl/lib/libssl.1.0.0.dylib (LoadError)
  Referenced from: /rails_project/vendor/bundle/ruby/2.7.0/gems/mysql2-0.5.3/lib/mysql2/mysql2.bundle
  Reason: image not found - /rails_project/vendor/bundle/ruby/2.7.0/gems/mysql2-0.5.3/lib/mysql2/mysql2.bundle

あれ?
起動しない

LoadErrorになっている。
opensslのライブラリが参照できなくなっている

opensslのバージョンが上がっていることに気づく

エラー内容をGoogleで調べているとライブラリを読み込ませると動く模様

mysql2をインストールしたときに参照しているopensslを参照できないようなので

一旦mysql2は削除して入れ直すことに

$ brew info openssl

を確認すると

  set -gx LDFLAGS "-L/usr/local/opt/openssl@1.1/lib"
  set -gx CPPFLAGS "-I/usr/local/opt/openssl@1.1/include"

とあり、ライブラリのパスが確認できます。

bindle 時に利用できるように以下のように config に設定します。

$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"

mysql2を入れ直すためアンインストールします。

$ bundle exec gem uninstall mysql2

再度インストール

$ bundle install

起動してみます。

$ rails c

インストール時にはライブラリを参照してくれてそうだけどエラーは解消しませんでした。

/rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `requir
e': dlopen(/rails_project/vendor/bundle/ruby/2.7.0/gems/mysql2-0.5.3/lib/mysql2/mysql2.bundle, 9): Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib (LoadError)
  Referenced from: /usr/local/opt/mysql/lib/libmysqlclient.20.dylib
  Reason: image not found - /rails_project/vendor/bundle/ruby/2.7.0/gems/mysql2-0.5.3/lib/mysql2/mysql2.bundle

rails cでライブラリの古い方を見てしまうようです。
PATHなどを確認して古いopensslのライブラリを参照しないようにしてますが
いろいろ調べてるがこれ以上はわからない状態でした。

opensslのインストール先を確認

$ brew --prefix openssl
/usr/local/opt/openssl@1.1

PATHに古いopensslが残っていて古いバージョンを参照していました。

$ which openssl
/usr/local/opt/openssl/bin/openssl

PATHの設定を見直して新しいバージョンを参照させて、再度bundleしなおしても駄目でした。

お手上げ感があったのですが、
mysql2.bundleがイマイチよくわかっておらず調べていると

qastack.jp

どうもmysql自体のインストール時に参照するライブラリを決め打ちしてそうということがわかり

一旦mysqlを停止してみました。

$ sudo mysql.server stop
Password:
dyld: Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib
  Referenced from: /usr/local/Cellar/mysql/5.7.17/bin/my_print_defaults
  Reason: image not found
Shutting down MySQL
... SUCCESS!

あら、rails c したときと同じようなエラーが、、、

起動もできなくなったので、
opensslのバージョンを戻すことに

brew で uninstall すると 依存関係のあるformulaが多いので怒られるが
--ignore-dependenciesをつけるとアンインストールできます。

$ brew uninstall --ignore-dependencies openssl
Uninstalling /usr/local/Cellar/openssl@1.1/1.1.1f... (8,057 files, 18MB)

古いopensslのライブラリをインストールする

$ brew install https://github.com/tebelorg/Tump/releases/download/v1.0.0/openssl.rb
Updating Homebrew...

これで1.0.2tがインストールされた。
(他のやりかたでインストールできるかもしれないが調べきれてない。)

mysqlを起動してみる

$ sudo mysql.server start 

Starting MySQL
.. SUCCESS!

起動できた!

bundle configの内容をバージョンダウンしたopensslに合わせる(戻す)

$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib"

再度mysql2を入れ直す

$ bundle exec gem uninstall mysql2
$ bundle install

まとめ

結局opensslのバージョンを一旦戻すことにした。
おそくらbrew upgrade で影響のある formula を上げていけばいいのだが、
上げたくないものもあるので、悩ましいところ。

brew で formula の依存関係を確認することができる。
brew deps コマンドが使える
今回のgooglerの場合だと以下の通り

$ brew deps --tree googler
googler
└── python@3.8
    ├── gdbm
    ├── openssl@1.1
    ├── readline
    ├── sqlite
    │   └── readline
    └── xz

ということで python経由でopensslのバージョンアップがされてしまったと気付ける。

逆にopensslがどのformulaが利用してるかというと以下のように uses オプションが使える。

$ brew uses --installed  openssl
asdf                  glib                  imagemagick           mysql                 python@3.8            shared-mime-info      vim
docutils              gnupg                 libevent              poco                  rbenv                 tmux
erlang                gnutls                libheif               python                ruby                  unbound

またautoupgradeさせない方法もあった。

$ brew pin openssl

というようにするとバージョンが固定されるようです。 解除するには

$ brew unpin openssl

とすると解除できます。
積極的に活用しようと思います。

RailsのRubyバージョンを2.7.0から2.7.1に上げる

いつものようにRailsRubyバージョンを上げていこうかと思います。

まずはruby-buildの最新化します。

$ brew upgrade ruby-build

rubyをrbenvで管理しているのでrbenvで2.7.1をビルドします。

$ rbenv install 2.7.1

Rails root の.ruby-version を 2.7.1 に変更

$ ruby -v
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin18]
$ bundle --version
Bundler version 2.1.4

Gemfileのrubyのバージョンを変更

ruby '2.7.1'

.ruby-versionとGemfileのバージョンをあわせてbundle installします。

$ bundle install

rails c を起動するとエラーが表示されました。

Traceback (most recent call last):
        15: from bin/rails:3:in `<main>'
        14: from bin/rails:3:in `require_relative'
        13: from /rails_project/config/boot.rb:4:in `<top (required)>'
        12: from /rails_project/config/boot.rb:4:in `require'
        11: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/setup.rb:2:in `<top (required)>'
        10: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/setup.rb:2:in `require_relative'
         9: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap.rb:4:in `<top (required)>'
         8: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap.rb:4:in `require_relative'
         7: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache.rb:74:in `<top (required)>'
         6: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache.rb:74:in `require_relative'
         5: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/store.rb:4:in `<top (required)>'
         4: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/explicit_require.rb:41:in `with_gems'
         3: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/store.rb:4:in `block in <top (required)>'
         2: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/store.rb:4:in `require'
         1: from /rails_project/vendor/bundle/ruby/2.7.0/gems/msgpack-1.3.3/lib/msgpack.rb:9:in `<top (required)>'
/rails_project/vendor/bundle/ruby/2.7.0/gems/msgpack-1.3.3/lib/msgpack.rb:9:in `require': cannot load such file -- msgpack/2.7/msgpack (LoadError)
        16: from bin/rails:3:in `<main>'
        15: from bin/rails:3:in `require_relative'
        14: from /rails_project/config/boot.rb:4:in `<top (required)>'
        13: from /rails_project/config/boot.rb:4:in `require'
        12: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/setup.rb:2:in `<top (required)>'
        11: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/setup.rb:2:in `require_relative'
        10: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap.rb:4:in `<top (required)>'
         9: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap.rb:4:in `require_relative'
         8: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache.rb:74:in `<top (required)>'
         7: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache.rb:74:in `require_relative'
         6: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/store.rb:4:in `<top (required)>'
         5: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/explicit_require.rb:41:in `with_gems'
         4: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/store.rb:4:in `block in <top (required)>'
         3: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/store.rb:4:in `require'
         2: from /rails_project/vendor/bundle/ruby/2.7.0/gems/msgpack-1.3.3/lib/msgpack.rb:8:in `<top (required)>'
         1: from /rails_project/vendor/bundle/ruby/2.7.0/gems/msgpack-1.3.3/lib/msgpack.rb:11:in `rescue in <top (required)>'
/rails_project/vendor/bundle/ruby/2.7.0/gems/msgpack-1.3.3/lib/msgpack.rb:11:in `require': incompatible library version - /rails_project/vendor/bundle/ruby/2.7.0/gems/msgpack-1.3.3/lib/msgpack/msgpack.bundle (LoadError)
        16: from bin/rails:3:in `<main>'
        15: from bin/rails:3:in `require_relative'
        14: from /rails_project/config/boot.rb:4:in `<top (required)>'
        13: from /rails_project/config/boot.rb:4:in `require'
        12: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/setup.rb:2:in `<top (required)>'
        11: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/setup.rb:2:in `require_relative'
        10: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap.rb:4:in `<top (required)>'
         9: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap.rb:4:in `require_relative'
         8: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache.rb:74:in `<top (required)>'
         7: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache.rb:74:in `require_relative'
         6: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/store.rb:4:in `<top (required)>'
         5: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/explicit_require.rb:40:in `with_gems'
         4: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/explicit_require.rb:44:in `rescue in with_gems'
         3: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/store.rb:4:in `block in <top (required)>'
         2: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/store.rb:4:in `require'
         1: from /rails_project/vendor/bundle/ruby/2.7.0/gems/msgpack-1.3.3/lib/msgpack.rb:9:in `<top (required)>'
/rails_project/vendor/bundle/ruby/2.7.0/gems/msgpack-1.3.3/lib/msgpack.rb:9:in `require': cannot load such file -- msgpack/2.7/msgpack (LoadError) 17: from bin/rails:3:in `<main>'
        16: from bin/rails:3:in `require_relative'
        15: from /rails_project/config/boot.rb:4:in `<top (required)>'
        14: from /rails_project/config/boot.rb:4:in `require'
        13: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/setup.rb:2:in `<top (required)>'
        12: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/setup.rb:2:in `require_relative'
        11: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap.rb:4:in `<top (required)>'
        10: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap.rb:4:in `require_relative'
         9: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache.rb:74:in `<top (required)>'
         8: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache.rb:74:in `require_relative'
         7: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/store.rb:4:in `<top (required)>'
         6: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/explicit_require.rb:40:in `with_gems'
         5: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/explicit_require.rb:44:in `rescue in with_gems'
         4: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/store.rb:4:in `block in <top (required)>'
         3: from /rails_project/vendor/bundle/ruby/2.7.0/gems/bootsnap-1.4.6/lib/bootsnap/load_path_cache/store.rb:4:in `require'
         2: from /rails_project/vendor/bundle/ruby/2.7.0/gems/msgpack-1.3.3/lib/msgpack.rb:8:in `<top (required)>'
         1: from /rails_project/vendor/bundle/ruby/2.7.0/gems/msgpack-1.3.3/lib/msgpack.rb:11:in `rescue in <top (required)>'
/rails_project/vendor/bundle/ruby/2.7.0/gems/msgpack-1.3.3/lib/msgpack.rb:11:in `require': incompatible library version - /rails_project/vendor/bundle/ruby/2.7.0/gems/msgpack-1.3.3/lib/msgpack/msgpack.bundle (LoadError)

msgpackの参照してるライブラリで参照エラーになってしまうようです。

私の場合はbundle install の際に path を vendor/bundle に設定しているので、
vendor/bundle/ruby/2.7.0 配下を削除して bundle install しなおします。
今回の2.7.1の場合, patchバージョン変更のため2.7.0のディレクトリがそのまま使われるようです。

Dockerなどでvendor配下を共有している場合も同様の事象に遭遇する可能性があるので
一旦削除して再インストールすると良いかもしれなれないです。

bundle upgrade の helpをみてみると options --ruby の項目があり

  --ruby Update the locked version of Ruby to the current version of Ruby.

と記載されていたので、 試してみたのですが 同様のエラーになって動作しなかったので使い方が違いそうです。

テストがある方は rails testなどでテストして動作確認してください。

$ rails test

ghコマンドとfzfコマンドのbind、previewを使って開発フローを見直す

概要

よく利用するコマンドが洗練されている方がいいのですが、
時間をかけて修正する大変でなかなか重い腰が上がらないです。

基本的にGitHubを使っての開発が多いのですが、 コマンドライン(以下CLI)とブラウザの行き来が多いです。

pecoを使っていたのですが、 fzfのpreviewが便利そうだったので previewを使いつつghと連携して改善してみました。 調べているうちにfzfのbindも便利そうなので使ってみました。

フローの概要

改善前のフロー

既存のissue prを確認するフロー

  • (ブラウザ) PullRequest(以下PR)を確認
  • (ブラウザ) PRからブランチを確認
  • (CLI) ブランチをチェックアウト
  • (CLI) コード修正、動作確認
  • (CLI) git add/commit/push
  • (ブラウザ) PRを確認

新規に開発するフロー

  • (ブラウザ) issueの確認
  • (CLI) ブランチを作成
  • (CLI) コード修正、動作確認
  • (CLI) git add/commit/push
  • (ブラウザ) PRを作成

改善後のフロー

既存のissue prを確認する場合

  • (CLI) PullRequest(以下PR)を確認 (CLI)
  • (CLI) PRのブランチをチェックアウト(CLI)
  • (CLI) コード修正、動作確認
  • (CLI) git add/commit/push
  • (ブラウザ) PRを確認

新規に開発する場合

  • (CLI) issueの確認
  • (CLI) ブランチを作成
  • (CLI) コード修正、動作確認
  • (CLI) git add/commit/push
  • (ブラウザ) PRを作成

改善した点

ghがissue, PRの一覧確認ができるので
fzf と組み合わせてブラウザの確認をCLIに変更します。
最後のPR確認はブラウザで画像をアップすることもあるのでCLIへの移行は難しそうです。

使用したコマンドのバージョン

command version
gh 0.6.4
fish 3.1.0
git 2.26.0
fzf 0.21.0

コマンド別の修正内容

改善のためにfish shellのfunctionを作成しています。 作成したfunctionは以下の3通りです。

  • issue確認のfunction
  • PR確認のfunction
  • git statusのfunction

issue確認のfunction

実際にfunctionを見ていきましょう。

function fzf_git_issue
  set -l query (commandline --current-buffer)
  if test -n $query
    set fzf_query --query "$query"
  end

  set -l base_command gh issue list --limit 100
  set -l bind_commands "ctrl-a:reload($base_command --state all)"
  set bind_commands $bind_commands "ctrl-o:reload($base_command --state open)"
  set bind_commands $bind_commands "ctrl-c:reload($base_command --state closed)"
  set -l bind_str (string join ',' $bind_commands)

  set -l out ( \
    command $base_command | \
    fzf $fzf_query \
        --prompt='Open issue list >' \
        --preview "gh issue view {1}" \
        --bind $bind_str \
        --header='C-a: all, C-o: open, C-c: closed' \
  )
  if test -z $out
    return
  end
  set -l issue_id (echo $out | awk '{ print $1 }')
  commandline "gh issue view -w $issue_id"
  commandline -f execute
end

gh を使ってopenなissueリストを一覧化してfzfに食わせています。
fzfpreviewgh issue view の内容を表示しています。
基本的にはCLIから確認し詳細を確認して、Enterでブラウザが開きます。

デフォルトではopenのissueだけを確認できるようにしておき、
issueのステータスがclosedを確認したい場合はCtrl-cで一覧をリフレッシュしています。
同様にすべてのステータスを確認したい場合はCtrl-aでリフレッシュしています。

選択したissueでEnterするとブラウザで確認できるようにしています。

PRの確認function

PRの確認も基本的にissueと同じように作っています。

function fzf_git_pull_request
  set -l query (commandline --current-buffer)
  if test -n $query
    set fzf_query --query "$query"
  end

  set -l base_command gh pr list --limit 100
  set -l bind_commands "ctrl-a:reload($base_command --state all)"
  set bind_commands $bind_commands "ctrl-o:reload($base_command --state open)"
  set bind_commands $bind_commands "ctrl-c:reload($base_command --state closed)"
  set bind_commands $bind_commands "ctrl-g:reload($base_command --state merged)"
  set bind_commands $bind_commands "ctrl-a:reload($base_command --state all)"
  set -l bind_str (string join ',' $bind_commands)

  set -l out ( \
    command $base_command | \
    fzf $fzf_query \
        --prompt='Select Pull Request>' \
        --preview="gh pr view {1}" \
        --expect=ctrl-k,ctrl-m \
        --header='enter: open in browser, C-k: checkout, C-a: all, C-o: open, C-c: closed, C-g: merged, C-a: all' \
  )
  if test -z $out
    return
  end
  set -l pr_id (echo $out[2] | awk '{ print $1 }')
  if test $out[1] = 'ctrl-k'
    commandline "gh pr checkout $pr_id"
    commandline -f execute
  else if test $out[1] = 'ctrl-m'
    commandline "gh pr view --web $pr_id"
    commandline -f execute
  end
end

ghをつかってPRの一覧を取得しています。
こちらもデフォルトだとステータスがopenのものに絞られている為、
マージ済みものも確認する場合はCtrl-gで表示し直していたりします。
PRの場合は特にマージ済みの情報を見たくなったりしているので初期コマンドをallで確認してもいいかもしれないです。
件数は使っているうちに変更したくなるやもしれませんが
デフォルトだと30件なので一旦は100件にしています。

ここで
Ctrl-kgh pr checkout <PRのid> を使ってcheckoutできます。
このコマンドは便利ですね。gh 様々です。
ブラウザでPRを確認してbranch名をコピーしてブランチの切り替えをやる作業をスムーズにすることができます。

git statusのfunction

git statusgstとしてaliasに設定していたのですが、
fzfのpreview、bindを組み合わせて改善しています。

function gst --description 'git status -s'
  if ! is_git_dir
    return
  end
  set -l base_command git status -s
  set -l bind_reload "reload($base_command)"
  set -l bind_commands "ctrl-a:execute-silent(git add {2})+$bind_reload"
  set bind_commands $bind_commands "ctrl-u:execute-silent(git restore --staged {2})+$bind_reload"
  set -l bind_str (string join ',' $bind_commands)

  set -l out (command $base_command | \
    fzf --preview="git diff {2}" \
        --expect=ctrl-m,ctrl-r,ctrl-v,ctrl-c \
        --bind $bind_str \
        --header='C-a: add, C-u: unstage, C-c: commit, C-m(Enter): mv, C-r: rm, C-v: edit' \
  )
  [ $status != 0 ]; and commandline -f repaint; and return

  if string length -q -- $out
    set -l key $out[1]
    set -l file (echo $out[2] | awk -F ' ' '{ print $NF }')

    if test $key = 'ctrl-m'
      commandline -f repaint
      commandline "git mv $file "
    else if test $key = 'ctrl-r'
      commandline "git rm $file "
      commandline -f execute
    else if test $key = 'ctrl-v'
      commandline "$EDITOR $file"
      commandline -f execute
    else if test $key = 'ctrl-c'
      commandline "git commit -v"
      commandline -f execute
    else
      commandline -f repaint
    end
  end
end

git status -s の結果を fzf に食わせています。

preview には git diff <選択ファイル> で差分内容が表示されます。

fzfのbindでいくつかのコマンドを設定しています。
Ctrl-agit add <選択ファイル> して fzf をリフレッシュしています。
Ctrl-ugit restore --staged <選択ファイル> して fzf をリフレッシュしています。
Ctrl-cgit commit -v します。
この3つの操作で基本的にcommitまで持っていくようにしています。

あとgit statusの内容を見ていてdiffなどから修正したいことがあったので、
Ctrl-v で エディタでのファイル編集を呼び出しています。私の場合はnvimになります。

付属としてgit rm/mvを用意していますが、おらくgit statusする前にコマンド打ちたくなりそうかな思われます。

まとめ

fzfのpreviewは他の人のブログを確認すると有効に利用している人を多々見かけます。 すべてをCLIで収めるのは難しかと思いますが、極力CLIで済ませることができそうです。

特にbindはブログに記載している人を見かけてなかったので これを見て利用してくれる方が出てくれば嬉しい限りです。

続 iTerm2でHexコードを送ってfishでCmd+[とCmd+]をディレクトリの戻る進むに対応させる

概要

以前の記事でfishのbindを利用して対応しました。 このbindによってペーストが壊れてしまい対応が難しいとわかったので
別の方法でiTerm2を使って対応しました。

経緯

[github.com

ここで貼り付けた文字列が壊れると質問したのですが

bind \e\[ 'do something'

がbindで設定を禁止させるべきかもと回答を頂きました。

現状では設定しなのがよいとのことなので、
fishで既にprevd-or-backward-wordがbindされているキーを
iTerm2で入力したCmd+[と]を別のキーとして入力することにしてみました。

設定

fishのbind確認

fishでnextd-or-forward-wordがbindされているキーを確認してみましょう。

$ bind --key | grep nextd-or-forward-word
bind --preset \e\eOC nextd-or-forward-word
bind --preset \e\e\[C nextd-or-forward-word
bind --preset \eO3C nextd-or-forward-word
bind --preset \e\[3C nextd-or-forward-word
bind --preset \e\[1\;3C nextd-or-forward-word
bind --preset \e\[1\;9C nextd-or-forward-word

次にprevd-or-backward-wordを確認します。

$ bind --key | grep prevd-or-backward-word
bind --preset \e\eOD prevd-or-backward-word
bind --preset \e\e\[D prevd-or-backward-word
bind --preset \eO3D prevd-or-backward-word
bind --preset \e\[3D prevd-or-backward-word
bind --preset \e\[1\;3D prevd-or-backward-word
bind --preset \e\[1\;9D prevd-or-backward-word

今回はAlt+←と→でディレクトリの行き来ができるとわかったので活用したいと思います。 Alt+← が \e[1\;3D で Alt+→ が \e[1\;3C です。

これを踏まえてiTerm2の設定を行います。

いくつか設定されていますが、このままだと何が設定されているのかよくわからない状態です。

iTerm2の設定

Cmd+[と]は他のキーに割り当ててもよいので
既にバインドされているキーに割り当ててしまいましょう。
iTerm2のPreferencesからProfilesのタブを選択して
Keysのタブを選択します。ここまでは以前と同じです。

fishにはbindするキーを確認するコマンド fish_key_reader があります。

Alt+← を確認すると

$ fish_key_reader
Press a key

              hex:   1B  char: \c[  (or \e)
(  0.144 ms)  hex:   5B  char: [
(  0.084 ms)  hex:   31  char: 1
(  0.130 ms)  hex:   3B  char: ;
(  0.130 ms)  hex:   33  char: 3
(  0.077 ms)  hex:   44  char: D
bind \e\[1\;3D 'do something'

Alt+→を確認すると

$ fish_key_reader
Press a key

              hex:   1B  char: \c[  (or \e)
(  0.122 ms)  hex:   5B  char: [
(  0.118 ms)  hex:   31  char: 1
(  0.032 ms)  hex:   3B  char: ;
(  0.056 ms)  hex:   33  char: 3
(  0.159 ms)  hex:   43  char: C
bind \e\[1\;3C 'do something'

1B 5B 31 3B 33 44
1B 5B 31 3B 33 43

だとわかるのでiTerm2に設定します。

f:id:hotoolong:20200407000434p:plain
keys_settting
こんな感じです。

まとめ

これらの設定で対応ディレクトリ移動が少し快適になりました。
fishで今後 "\e[" のbindは禁止されるかもしれませんが
いい方法で解決してくれると嬉しいです。

参考URL

対話型コンソールアプリ制作初級者に教えたい「CSI」 - Qiita

ASCII Table

fishでCmd+[とCmd+]を使って移動したディレクトリに移動する

こちら記事を書いたのですが、設定するとペーストが壊れてしまう事象が発生してしまいました。 事象が解消してから設定することをおすすめします。

概要

Macでブラウザの戻る進むを Cmd+[ と Cmd+] で行うのと同じように
Slackのチャネルの戻る進むを Cmd+[ と Cmd+] で行うのと同じように
fishでのディレクトリの移動を Cmd+[ と Cmd+] で行いたい。

fish

config の設定

bind \e\[ prevd-or-backward-word
bind \e\] nextd-or-forward-word

configに上記を追加します。
\eはEscです。
ここではCmdではないです。本来であればCmd+[をそのまま認識させたいのですが、
やり方がなさそうなので、Escにしています。
別のなにかに置き換えていても問題ないです。
今回はこのようにしています。

prevd-or-backward-wordとは

次にprevd-or-backward-wordの中身を確認しておきます。
(※ ここは読み飛ばしてもらっても問題ないです。)

function prevd-or-backward-word
  set -l cmd (commandline)
  if test -z "$cmd"
    prevd
    commandline -f repaint
  else
    commandline -f backward-word
  end
end

コマンドが入力状態をチェックして入力されてなかったら
prevdを発行して再描画しています。
prevdは以前にいたディレクトリへ移動するコマンドです。
backward-wordは1文字戻るコマンドです。入力中の場合は一文字戻るようにしているようです。

やりたいことだけだと

prevd
commandline -f repaint

ここの処理だけです。 このコマンドを別途切り出してもfunctionにしてもいいのですが、
今回はprevd-or-backward-wordが既に実装されているのでこれを使うことにしました。

iTerm2の設定

最近はiTerm2のショートカットが勝手に起動して少し使いづらさを感じていて移行しようか迷い中ですが、 今回はiTerm2の設定です。

Cmd+[とCmd+]を入力してEsc+[とEsc+]に変換する設定を追加しています。

iTerm2のPreferencesからProfilesのタブを選択して
Profileを選択(私の場合はDefault)
Keysのタブを選択して一覧の下にある+ボタンを押して設定を追加します。
Keyboard ShortcutにはCmd[を設定してActionにSend Escape Sequenceを設定して Esc + [ を設定すればOKです。

まとめ

これでCmd+[とCmd+]で戻ることができます。
..を押すことが度々ありますが、これでCmd+[で快適になりそうです。
ディレクトリ..での移動が多い人はやってみてはどうでしょうか。

Qiitaの記事をmarkdownファイルで保存する

概要

Qiitaの退会がいて既にみれなくなっている記事もあるかと思いますが、
ストックしてたい記事がなくなるととても残念です。 まだ公開されているものはlocalに保存しておくのも手ではないでしょうか。

やりかた

qiita.com こちらを参考にして保存したい記事のURLを取得してファイルに保存しておきます。
今回はqiita_urls.txtをつくり、 取得したいURLをファイルに複数行設定して以下のコマンドを実行しました。

cat qiita_urls.txt | xargs -n1 -I{} bash -c "curl -sS {}.md > \$(echo {} | awk -F / '{print \$NF}').md"

簡単ですね。

まとめ

これで一旦保持したい記事はローカルに残すことができそうです。