Denoでcommitizen的なツールを作った

Denoでcommitizen互換のツールを作りました。

f:id:Ryooooooga:20220121200521p:plain

github.com

commitizenはGitのコミットメッセージをいい感じにするためのツールです。

commitizenのようなツールには公式である commitizen/cz-cli の他に、streamich/git-cz や Go製の lintingzhen/commitizen-go があります。

以前は cz-cli を使用していたのですが、これは Node.js 製であるため、グローバルに npm install する必要があります。 しかし、私はグローバルに npm install, gem install, pip install をあまりしたくないため、これに若干の不満がありました。

commitizen-go はNode.jsに依存しませんが、あまり開発が活発ではなさそうな上、入力にEmacsキーバインドが使用できないことから移行先としては若干心許ないように思えたため、似たようなものを自作することにしました。

commitizen-deno について

commitizen-deno は名前の通り Deno で作成された commitizen 互換のツールです。

ユーザからの入力の取得に FZF を用いている点が大きな特徴です。

使用感は上のツイートのような感じです。

インストール方法

実行には Deno と FZF を要求します。

git clone してPATHを通すか、zinit などのプラグインマネージャを用いてダウンロードするかしてください。

# ~/.zshrc
zinit wait lucid light-mode as'program' for 'Ryooooooga/commitizen-deno'

使用方法

git commit の代わりに commitizen-deno を実行してください。

Gitのサブコマンドとして実行したい場合は、以下のように alias を登録すると良いでしょう。

$ git config --global alias.cz '!commitizen-deno --'

$ git cz

コンフィグ

$XDG_CONFIG_HOME/commitizen-deno/config.yaml (あるいは ~/.config/commitizen-deno/config.yaml) に設定を書くことで入力項目やコミットメッセージをカスタマイズできます。

例えば以下のように設定することで入力項目などを日本語化できます (日本語訳は cz-conventional-changelog-ja を参考にしています)。

# ~/. config/commitizen-deno/config.yaml
message:
  items:
    - name: type
      # description: Select the type of change that you're committing
      description: コミットする変更タイプを選択
      required: true
      form: select
      options:
        - name: feat
          # description: A new feature
          description: 新機能
        - name: fix
          # description: A bug fix
          description: バグ修正
        - name: docs
          # description: Documentation only changes
          description: ドキュメントのみの変更
        - name: style
          # description: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
          description: フォーマットの変更 (コードの動作に影響しないスペース、フォーマット、セミコロンなど)
        - name: refactor
          # description: A code change that neither fixes a bug nor adds a feature
          description: リファクタリングのための変更 (機能追加やバグ修正を含まない)
        - name: perf
          # description: A code change that improves performance
          description: パフォーマンスの改善のための変更
        - name: test
          # description: Adding missing tests or correcting existing tests
          description: 不足テストの追加や既存テストの修正
        - name: build
          # description: "Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)"
          description: "ビルドシステムや外部依存に関する変更 (スコープ例: gulp, broccoli, npm)"
        - name: ci
          # description: "Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)"
          description: "CI用の設定やスクリプトに関する変更 (スコープ例: Travis, Circle, BrowserStack, SauceLabs)"
        - name: chore
          # description: Other changes that don't modify src or test files
          description: その他の変更 (ソースやテストの変更を含まない)
        - name: revert
          # description: Reverts a previous commit
          description: 以前のコミットに復帰

    - name: scope
      # description: What is the scope of this change (e.g. component or file name)
      description: "変更内容のスコープ (例:コンポーネントやファイル名): (enterでスキップ)"
      form: input

    - name: subject
      # description: Write a short, imperative tense description of the change
      description: 変更内容を要約した本質的説明
      required: true
      form: input

    - name: detail
      # description: Provide a longer description of the change
      description: "変更内容の詳細: (enterでスキップ)"
      form: input

    - name: breakingChange
      # description: Describe the breaking changes if exist
      description: "破壊的変更についての記述: (enterでスキップ)"
      prompt: "BREAKING CHANGE > "
      form: input

    - name: issue
      # description: 'Add issue references (e.g. "fix #123", "re #123".)'
      description: '関連issueを追記 (例:"fix #123", "re #123"): (enterでスキップ)'
      form: input

  template: "<%- type %><% if (scope) { %>(<%- scope %>)<% } %>: <%- subject %><% if (detail) { %>\n\n<%- detail %><% } %><% if (breakingChange) { %>\n\nBREAKING CHANGE: <%- breakingChange %><% } %><% if (issue) { %>\n\n<%- issue %><% } %>"

上の通り、コミットメッセージのテンプレート (.message.template) は EJS を用いて記述できます。

なぜDenoを選択したか

上に述べた既存のツールに対する不満点のために、ずっと以前から Node.js に依存しない形で commitizen を実装したいという思いがありました。

最初はShellスクリプトを用いて実装することを考えていたのですが、カスタマイズ性を確保する点で実装がしんどくなりそうなため、踏ん切りがつかずにいました。

最近になり dotfiles 内で Deno をインストールする方針にしたため、Tmuxinatorの代替ツールをDenoで作った - 茅の下 のようにShellスクリプトの代替としてDenoを選択できるようになったことから、今回もDenoを用いて実装することにしました。

なぜFZFを利用したか

自作する以上、既存ツールと同等かそれ以上の使用体験が得られなければなりません。 具体的には、以下のような目標を達成する必要がありました。

しかし、Denoは現状ライブラリの選択肢が少なく、また現在あるライブラリを用いたとして上の要求を満たせるとは限りません。 そこで、fuzzy matchが可能でEmacsキーバインドを使用でき、さらにカスタマイズの余地がある FZF を入力ツールとして用いることにしました。

FZF については本ブログでも以下の記事で紹介していますが、FZF は本来 fuzzy finder、つまり与えられた選択肢からの絞り込み・選択を得意とするツールです。

ryooooooga.hateblo.jp

コミットメッセージのような、選択肢に依らない自由入力に FZF を用いるというアイデアdenisidoro/navi から得たものです。

自由入力に FZF を用いる

fzf--print-query オプションを用いると、選択された選択肢のみでなく、fuzzy match のためのユーザの入力クエリを出力として得られます。 denisidoro/navi はこれを用いてユーザの自由入力を実現しています。 commitizen-deno のコミットメッセージの入力は、naviを参考に作成しました。

コミットメッセージのプレビュー

commitizen-deno は、FZF の preview-window を用いて、入力中のコミットメッセージをリアルタイムにプレビューできるようになっています。

f:id:Ryooooooga:20220121211338p:plain
コミットメッセージのプレビュー (画面下部)

プレビューにはShellコマンドを渡さなければならない都合上、このような本来の用途から若干離れた用途のためにはエスケープシーケンスなどの扱いに若干の工夫が必要になります。

まとめ

Deno を用いて commitizen の互換ツールを作成しました。Shellスクリプトを書きたくない程度のものを作る際にDenoは良い選択肢になります。

Node.jsに依存したくない一方、FZFやDenoに依存するのはもやもやする部分がありますが、自分用のツールなので許容しています。