React で Octicon を使うためのコンポーネントライブラリ書いた

OcticonsGitHubオープンソースで配布しているアイコンセットです.

Octicons は以前はウェブフォントを利用してつくられていたのですが,最新のnpm の octicons パッケージの中身を見る限りでは,最近では SVG での配布になったようです. 以前は classocticon octicon-alert などを指定すれば使えていたのですが,それができなくなってしまいました.

React を使って書いているので Octicon の React 向けコンポーネントを探してみると react-octicon がヒットするのですが,

  • ウェブフォント時代の古い Octicon(v4系)のみサポート
  • webpack が必須(CSS ローダを使って Octicon の CSS をロードしている)

という点で私のユースケースに合いませんでした.

というわけで,最新の Octicon(v7.0.1)で動く React コンポーネントライブラリをつくりました.

github.com

特徴は

  • 依存なし: octicons パッケージの SVG を React コンポーネント内に直接埋め込んでいるので依存パッケージなし.よって特定の bundler にも依存していない
  • TypeScript 対応済み.本体も TypeScript で書かれてます

使い方

npm パッケージとして配布しています.

$ npm install --save react-component-octicons

でインストールできます.

import * as React from 'react';
import { render } from 'react-dom';
import Octicon from 'react-component-octicons';

render(
    <div>
        <Octicon name="alert" />
        <Octicon name="star" />
    </div>,
    document.getElementById('root'),
);

のように <Octicon/> コンポーネントとして使えます.ここでは TypeScript で書いていますが,もちろん JavaScript からも使えます.

name プロパティに Octicon のアイコン名を指定するだけです.

追記:アイコンのサイズを変えられるようになりました

import * as React from 'react';
import { render } from 'react-dom';
import Octicon from 'react-component-octicons';

render(
    <div>
        // Normal size
        <Octicon name="alert" />

        // Twice bigger
        <Octicon name="star" zoom="x2" />

        // Size 100px x 100px
        <div style={{width: '100px', height: '100px'}}>
            <Octicon name="flame" zoom="100%" />
        </div>
    </div>,
    document.getElementById('root'),
);

x{N}{N} は整数か小数点数)とすると N 倍のサイズのアイコンになります(例:x4, x1.5). また,{N}%{N} は0〜100)とすると親の要素に対して N% のサイズになります.なので 100% を指定すれば親のサイズに合わせたサイズのアイコンになります.

Typo Safety

name プロパティは 文字列リテラル型で定義しているので, 間違ったアイコン名を指定しているとコンパイルエラーになります.

render(
    <Octicon name="allow-right" />,
    document.getElementById('root'),
);

例えば上記のアイコン名は arrow-rightallow-right にタイポしていますが,これをコンパイルすると

test.tsx(5,17): error TS2322: Type '{ name: "allow-right"; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<Octicon> & Readonly<{ children?: ReactNode; }> & R...'.
  Type '{ name: "allow-right"; }' is not assignable to type 'Readonly<OcticonProps>'.
    Types of property 'name' are incompatible.
      Type '"allow-right"' is not assignable to type 'OcticonSymbol'.

のようにエラーになります.

まとめ

React で Octicon を使うためのコンポーネントライブラリ react-component-octicons をつくりました. 自分が使う用途でつくったものですが,もし Octicon を React で使う機会があれば是非使ってみてください.

GitHub のプルリクで blame する ghpr-blame.vim つくった

先日 kazuho さんが git blame でプルリクを表示するスクリプトをつくってらっしゃって,便利そうだったので Vim プラグインをつくってみました. ファイルの各行がどのプルリクで変更されたかを確認し,気になるプルリクはその場で詳細を確認することもできます.

github.com

スクリーンショット

使い方

インストールはお好みの Vim プラグインマネージャを使うなどしてください.

1. ファイルを開いて :GHPRBlame を実行

:GHPRBlame を実行すると裏で git-blame が走り,カレントバッファのファイルの各行のプルリク情報を git blame --line-porcelain で引っ張ってきます. 引っ張ってきた情報を元にカレントバッファの左に細長い一時バッファが開き,そこに各行に紐付いたプルリク番号が表示されます(プルリクに紐付いていない行は何も表示されません). これによって,各行がどのプルリクによって入ったものかが分かります.

2. プルリクの詳細を見たい行で <CR> を押す

プルリクの詳細が見たい位置にカーソルを移動し,<CR>g:ghpr_show_pr_mapping で別のキーにも変更可能)を押すと,カレントバッファのウィンドウの右か下に一時バッファが開き,そのプルリクの情報(タイトル,URL,作成者,マージされた日付,本文)を確認することができます. <CR>を押した際に裏で GitHub API を叩いて対象のプルリクの情報を引っ張ってきているので,表示に少し時間がかかります.一応キャッシュしているので2回目以降同じプルリクは高速に表示できます.

3. 終了する

一番左のプルリク一覧のウィンドウを閉じると自動で右側のウィンドウも閉じ,キャッシュや<CR> マッピングを削除します.もしくは :GHPRBlameQuit で明示的に終了することもできます.

まとめ

ファイルの各行の変更をプルリク単位で調べられる ghpr-blame.vim をつくりました.これ系のは tig みたいに TUI なツールのほうが便利かなと思いつつ,結局コードを読む時は Vim で開いて追いながら読むことが多いので Vim プラグインとしてつくってみました. もし気になったらお試しください.

Goのコマンドラインツールをセルフアップデートするためのライブラリつくった

突然ですが,Goでコマンドラインツールを書く時,ツールの配布はどうしているでしょうか?

  1. go get でインストールできるようにする
  2. GitHub 上にリリースして,ダウンロードして使ってもらう
  3. システムのパッケージマネージャ(Homebrew など)を使う

などがメジャーかと思います.

ただ,これらの選択肢はどれも問題があります.

go get -u は常にリポジトリの HEAD をインストールしてしまうため,ユーザがインストールしたタイミングに依存したバイナリができてしまいます.これを避けるには dev ブランチを切ってそっちで開発する必要がありますが,Go のツールはそうなってないものが多く,どのタイミングで go get -u したら良いかユーザには容易に判断できません.また,仮に dev ブランチ運用したとしても依存ライブラリの更新のタイミングは制御できず,vendoring などを使う必要があります.

GitHub 上でのリリースは各バージョンごとにテストが通ったものをリリースノート付きでリリースできますが,ユーザが自発的にアップデートを確認し,手でバイナリをダウンロードしてくる必要があります.

システムのパッケージマネージャは上記の問題を解決してくれますが,プラットフォームに依存し,アップデート時に余計な手間も出ます(例えば Homebrew なら homebrew-core へのプルリクなど).

Go は全てを静的リンクした1つのバイナリを簡単にクロスコンパイルできる利点を持っているため,これをうまく使えないかと思い,go-github-selfupdateをつくりました.この記事ではその紹介をします.

GoDoc Badge TravisCI Status AppVeyor Status Codecov Status

github.com

go-github-selfupdate とは

go-github-selfupdate とは,GitHub 上にある最新のリリースを検知して,自分自身のバージョンよりも高ければ自動で自分自身のバイナリを更新する(セルフアップデート)機能を提供するためのライブラリです.

以下のような流れでセルフアップデートを行います.

  1. GitHubReleases API を使い,プレリリースでない最新のリリースを Git のタグ名から判定してリリース物の URL を取得
  2. リリース物をダウンロードし,zip や gzip,tar.gz などの圧縮形式・アーカイブ形式をファイル名から判定し,自動で展開・解凍して目的のバイナリを引っ張ってくる
  3. inconshreveable/go-update を使って自身の実行ファイルをダウンロードした新しいものに差し替える

これによって,GitHub で公開している最新の安定版バイナリにそのコマンドから自動検知・更新することができます.

Travis CI や AppVeyor を使って Linux, Windows, macOS でテストし,正常系だけでなく異常系もテストしています(カバレッジ90%以上). また,inconshreveable/go-update を使っているので,実行ファイルの差し替えに失敗した場合には自動で元の実行ファイルにロールバックしてくれます. zip,gzip,tar.gz の解凍・展開には Go の標準ライブラリを使っています.

使い方

お試し CLI

テストにも使っているお試しの CLI があるので,下記のように試せます.これはリリースページのバイナリにセルフアップデートする(v1.2.3 → v1.2.4)例です.

$ go get -u github.com/rhysd/go-github-selfupdate/tree/master/cmd/selfupdate-example

$ # バージョンを確認
$ selfupdate-example -version
v1.2.3

$ # セルフアップデートを実行
$ selfupdate-example -selfupdate

$ # アップデートされている
$ selfupdate-example -version
v1.2.4

$ # もちろん再度実行してもすでに最新なので何も起きない
$ selfupdate-example -selfupdate
$ selfupdate-example -version
v1.2.4

コード例

セルフアップデートの各段階の処理を関数として公開していますが,一番簡単なのは selfupdate.UpdateSelf() を使うことです.

import (
    "log"
    "github.com/blang/semver"
    "github.com/rhysd/go-github-selfupdate/selfupdate"
)

const version = "1.2.3"

func doSelfUpdate() {
    v := semver.MustParse(version)
    latest, err := selfupdate.UpdateSelf(v, "owner/repo")
    if err != nil {
        log.Println("Binary update failed:", err)
        return
    }
    if latest.Version.Equals(v) {
        // latest version is the same as current version. It means current binary is up to date.
        log.Println("Current binary is the latest version", version)
    } else {
        log.Println("Successfully updated to version", latest.Version)
        log.Println("Release note:\n", latest.ReleaseNotes)
    }
}

(後ほど説明しますが)このライブラリではバージョンの大小比較のためにセマンティックバージョニングを前提としています. UpdateSelf() はツールの現在のバージョンと,ツールがホストされているリポジトリの情報(owner/repo)を受け取り,更新した結果のリリースを表す構造体(selfupdate.Release)とエラーを返します.

エラーはダウンロードしてきたファイルが破損していた場合など,セルフアップデートが実行できなかった場合に返されます.

現在のツールのバージョンが最新だった場合はエラーではないので,latestVersionフィールドに現在のバージョンと同じ値が返されます. なので,アップデート後のバージョンと今のバージョンを比較する(latest.Version.Equals(v))ことで実行ファイルの差し替えが行われたかを知ることができます.latest にはバージョンの他にリリースノートやリリースページの URL なども含まれるため,必要な場合は適宜利用できます.

この(ユーザへの出力を含めて)14行だけで実行ファイルをアップデートする仕組みを組み込むことができました.

次にもう少し複雑な例を出してみます. バージョンアップデート前にユーザに「このバージョンにアップデートするけど良い?」と確認を取りたいとします.

import (
    "bufio"
    "github.com/blang/semver"
    "github.com/rhysd/go-github-selfupdate/selfupdate"
    "log"
    "os"
)

const version = "1.2.3"

func confirmAndSelfUpdate() {
    latest, found, err := selfupdate.DetectLatest("owner/repo")
    if err != nil {
        log.Println("Error occurred while detecting version:", err)
        return
    }

    v := semver.MustParse(version)
    if !found || latest.Version.Equals(v) {
        log.Println("Current version is the latest")
        return
    }

    fmt.Print("Do you want to update to", latest.Version, "? (y/n): ")
    input, err := bufio.NewReader(os.Stdin).ReadString('\n')
    if err != nil || (input != "y\n" && input != "n\n") {
        log.Println("Invalid input")
        return
    }
    if input == "n\n" {
        return
    }

    if err := selfupdate.UpdateTo(latest.AssetURL, os.Args[0]); err != nil {
        log.Println("Error occurred while updating binary:", err)
        return
    }
    log.Println("Successfully updated to version", latest.Version)
}

少々長いですが,全体の流れとしては,UpdateSelf() の中でやっている処理をバラして,

  1. selfupdate.DetectLatest()GitHub 上での最新のリリースを検知してアップデートの必要があるかチェック
  2. ユーザに y/n で確認するプロンプトを出す
  3. selfupdate.UpdateTo() で実行ファイルをダウンロード,解凍・展開,置き換えする

というものです.特に難しい部分は無いはずです.リリースが無い場合は異常系では無いので,selfupdate.DetectLatest() はリリースがあったかどうかとエラーを別に返すようにしています.

リリース物の名前付けルール

どのバイナリがどの環境向けのものかを判別するために,下記のような名前付けでリリース物を配布しているという前提で実装しています.

{cmd}_{goos}_{goarch}{.ext}

{cmd}コマンドラインツールの名前です.この中に _ を含んでいても構いません. {goos} は OS の種類(runtime.GOOS),{goarch}アーキタイプruntime.GOARCH)です.{.ext} はファイルの拡張子で,何もなし(圧縮なし),.zip.gz.tar.gz のいずれかで,対応する圧縮・アーカイブが施されているものとします.

また,セパレータには _ の代わりに - が使え,Windows 限定で .exe.zip のように .exe が付いても大丈夫なようになっています(実行ファイルをそのまま圧縮しても良いように).

なので,foo-bar をコマンド名とすると下記のようなファイルはリリース物として認識されます.

  • foo-bar_linux_amd64
  • foo-bar_linux_amd64.zip
  • foo-bar_linux_amd64.tar.gz
  • foo-bar_linux_amd64.gz
  • foo-bar-linux-amd64.tar.gz

リリース名(Git のタグ名)の名前付けルール

go-github-selfupdateは Git のタグを見てそのリリースのバージョンを判定します(リリースタイトルではないので注意).

タグ名は正規表現で言うと \d+\.\d+\.\d+ を含んでいる必要があり,それより手前の文字列は削除されます.なので,v1.2.3ver1.2.3release-1.2.3などは全てバージョン 1.2.3 として認識されます.

この命名規則に合致しないもの(例えば nightly など)や,プレリリースに指定されているものなどは単に無視されます.

リリース物の構造

まとめると下記のような構造でリリースすれば正しくgo-github-selfupdateで認識できます.

コマンド名を foo-bar とすると,一例としてリリースは下記のようになります.

  • v1.2.4
    • foo-bar_linux_amd64.tar.gz
      • foo-bar
    • foo-bar_darwin_amd64.tar.gz
      • foo-bar
    • foo-bar_windows_amd64.exe.zip
      • foo-bar.exe
    • ...(他のプラットフォーム向けのバイナリ)
  • v1.2.3
    • foo-bar_linux_amd64.tar.gz
      • foo-bar
    • foo-bar_darwin_amd64.tar.gz
      • foo-bar
    • foo-bar_windows_amd64.exe.zip
      • foo-bar.exe
    • ...(他のプラットフォーム向けのバイナリ)

難しく考えなくても,gox でクロスビルドしてアーカイブし,ghrでリリースしたりしていれば大体この構造になっているのではないかと思います.

デバッグする

go-github-selfupdate内では(デフォルトで無効な)ログを仕込んでありますので,ツールのデバッグ時には下記のようにしてログを有効にすることで内部でどういう処理が行われているかを標準エラー出力に表示することができます.

selfupdate.EnableLog()

適用例

手前味噌ですが,いくつか自分のツールにはすでに組み込んでみています.

おまけ CLI ツール

go-github-selfupdateを使い,いくつか簡単なコマンドラインツールも作成しました.

  • detect-latest-release: 引数に与えられたリポジトリの最新のリリースを検知するコマンド
  • go-get-release: go get のような Go のコマンドをインストールするツール.go get と違い GitHub の最新のリリース物をダウンロードしてきて $GOPATH/bin に配置する

例えば下記のように ghr の安定版をインストールしたり,

$ go-get-release github.com/tcnksm/ghr
Command was updated to the latest version 0.5.4: /Users/rhysd/.go/bin/ghr

下記のように dot-github の最新バージョン(やリリースノートなど)を知ったりすることができます.

$ detect-latest-release rhysd/dot-github
1.3.0

go-github-selfupdateを下地として使っているので,飽くまで前章で解説した名前付けルールに従っているものでしか使えません(それ以外ではリリースが検知できないと思います).

tj/go-updateとの違い

実はすでに同じ目的のライブラリとして tj/go-update がありますが,下記の点が違います

tj/go-update は

  • Windows をサポートしていない
  • バージョンのプレフィックスは v のみ許可
  • プレリリースかどうかを見ない
  • テストが少ない(正常系の1ケースのみ)
  • GitHub だけでなく Apex というリリース置き場をサポートしている

まとめ

GitHub 上にある最新のリリースを検知して,自分自身のバージョンよりも高ければ自動で自分自身のバイナリを更新する(セルフアップデート)機能を提供するためのライブラリ,go-github-selfupdateを作成しました.これによって,配布者にとってはリリースの配布がより楽になり,ユーザにとってはリリースされた安定版に手軽にアップデートできるという利点があるのではないかと思います.