8
3

More than 1 year has passed since last update.

お正月休みにGitをインタラクティブに操作できるサブコマンドツール作ってきました

Posted at

年末からお正月にかけての時間で趣味でツールを作ってみました🎍
Gitのちょっと面倒な操作をインタラクティブに行えるGitのサブコマンドツールgit-exを作成してnpmに公開しています。

僕は普段はAndroidエンジニアとしてKotlinを書いていますが、趣味ではVimのカラースキーム作ったり、Goでツール作ったり、Rustに手を出したり、、、など色々しています。今回はJavaScriptのターミナルのライブラリがとても良さそうだったのでnode製のCLIツールを作ってみました😄!

このQiita記事では前半にツールの紹介と、後半で実装面を書いてみようと思います。

ツールの紹介

僕は普段Gitをターミナル上でコマンドで使っているのですが、たまにSourceTreeなどのGUIツールやTIGなどのツールが使いたくなるときがあります。
たとえば、ファイルパスをいちいちGitの引数に指定しなければならないときなどです。そういうときだけSource Tree起動するのも面倒ですし、TIGを使うんとなると操作を覚えないとなかなか使い勝手がよくありません。
もう少し、ターミナル上で簡易に使えるようなツールがあれば便利だなと思って開発してみました。

git-exをインストールすると、いちいちファイルパスを指定しないといけないようなちょっと面倒なGit操作をターミナル上でインタラクティブに操作することができるようになります。

npmで公開しているJavaScript製のツールになります。インストールは下記で可能です。

npm i -g @yasukotelin/git-ex

このツールをインストールすると git-ex コマンドが使えるようになるのですが、Gitはgit-xxx形式のコマンドをサブコマンドとして認識するので、git ex 形式で実行することが可能です。

サブコマンドとして認識されない場合は、一度ターミナルを再起動すると反映されると思います。 git-ex コマンドがそもそも見つからない場合は、npmのグローバルインストールモジュールへのパスが通っていないと思うので環境変数を確認してください。

git ex stage / unstage

ステージング操作をするときは通常ターミナルでステージングするには下記のようなコマンドを打ちます。

# Gitプロジェクト内のすべての変更をステージングする
git add -A

# こっち使う人も多いかも。
git add .

大抵のケースでは正直これで事足ります。ですが、特定のファイルをaddしたいときは結構面倒です。

git add ファイルパス

例えば変更が3ファイルあって、1ファイルはコミットに含めたくないみたいなケースです。この場合、毎回愚直に引数にパス指定をするか、一度add allしてから、省きたいファイルをresetするみたいな操作をする必要があり、かなり面倒です。
ましてや、自分はAndroid開発をしているのもあって、Java系のファイルパスの長さは地獄です。とても補完で選択はしてられません。

そこで git ex stage コマンドを使うとマルチセレクトでファイルをステージングすることができるのでとても便利です😍

image.gif

また逆の操作のアンステージも git ex unstage でできます。Gitってアンステージ処理をしようと思うと、そもそもコマンドが確かRESETかなんかで覚えにくいという部分もあります。個人的には git ex stage コマンドよりも重宝してます。

git ex stash

次はStashをよく使う人にもおすすめな git ex stash コマンドです。
Stashの基本的な操作をインタラクティブに選ぶ機能と、Stashの一覧から復元や破棄を選択して実行できる機能があります。

こんな感じです。

image.gif

Stashの一覧を見て、選択するだけでpopしています。
これをGitのコマンドでやろうと思うと、微妙にやり方忘れててhelp引いたり、Listのどの部分を引数に指定すればいいんだっけってなっていたので助かります。

あと、Gifだと確認しづらいですが、うっすらとpopやapplyなどにカーソル合わせた時に説明も右側に出しているので、popだっけapplyだっけってなっても安心です。

追記ですが、記事執筆中にStashの内容も機能追加しました😍

git ex discard

変更を削除したいとき、SourceTreeなどのGUIツールでは変更を破棄するDiscard処理(Reset処理)が簡単にできると思います。
ターミナルで変更を破棄したい場合は、 git checkout ファイル名 や、 git clean -df などでできますが、そのものずばりな操作な感じではないのでいつも迷います。

この git ex discard を使えば下のGif画像のようにインタラクティブに変更を破棄できます。

image.gif

PRレビューでちょっと試した差分を破棄したいときとかに便利です。

このDiscard処理は破棄になるのでGitの履歴には残りません。ご使用には十分ご注意ください。

git ex rm-merged

最後に、微妙に便利な、マージ済みブランチを削除できる git ex rm-merged コマンドを紹介します。

マージ済みのブランチをいつまでも残しておくと、checkout(switch)するときに無駄に選択肢が残っていたり、名前被りしていいことなかったりします。
GithubではPRをマージしたタイミングでマージ済みブランチを削除できるのでリモートブランチは削除しやすいです。ですがローカルは別途自分で git branch -d ブランチ名 しなくてはならないので億劫になりがちです。

そこでこの git ex rm-merged を使うとマージ済みブランチをリスト表示してくれてマルチセレクトで削除することができます。

image.gif

このGifでは1件なのですが、たくさんあって一括で削除したい時も複数選択できます。ちなみに、aをタイプするとallで全選択されます。
マージ済みブランチはmain(master)やdevelopも対象になるのですが、現状はハードコーディングで対象外にしています。親ブランチなどを誤って削除しないようにご注意ください。

実装面

nodeで動作するJavaScript製のツールです。最初はTypeScriptで実装していたのですが、紆余曲折あって今はJavaScriptで実装されています。

webpackやbabelなどのビルドツール的なものは一切使っていないシンプル構成になっています。

JavaScript(TypeScript)はだいぶ前に業務で触ってたことがある程度で、普段は書いてません。なので実装時間よりも結構色々調べるところのほうが時間がかかりました😅

JavaScriptとTypeScript

普段Kotlinを書いてますし、今までツールを書くときにはGoをよく使っています。
久しぶりに型定義のない言語も書いてみたくなったというのもあってTypeScriptではなくJavaScriptを選びました(笑)

正直それ以外の理由は基本的には特にないです。JavaScriptが書きたくなったので選んだって感じです🤣

それにしても、型定義しないで書くの、なんだか魔術感があってわくわくしますね😈
個人で書くには楽ちんで結構ありだよなとちょっと再認識もできました。

あとは、ここ最近のJavaScriptの進化も体感してみたいというのもありました。jQuery全盛時代から考えると本当に色々書きやすくなっていて、不満はなかったですね。。classも、自分が読んでいた本ではprototypeを使って頑張るイメージでしたが普通に使えますし、いつの間にかフィールドも普通に定義できるようになってるんですね。

ESモジュールとCommonJS

Reactの開発経験はあったのでES6は知っていましたが、CommonJSはよくわかっていませんでした(意識したことなかった)。

端的にまとめると、nodeはCommonJSなのでrequireを使ってモジュールをインポートしますが、ブラウザサイドはESモジュールなのでimportを使います。

// CommonJS
const a = require('module');

// ESモジュール
import a from 'a';

最初ここらへんの知識がなかったので微妙に苦戦しました。僕はESモジュールで書きたかったのですが、 nodeはESモジュールにも対応しているようだったので、package.jsonで指定しました。

{
  "type": "module",
}

これでnodeでもESモジュール形式になってimport形式になりました。
また、import側だけでなくてモジュールをexportする側の書き方もESモジュールとCommonJSで書き方が違うので注意が必要でした。

ESモジュール指定をしていれば下記のような形式で書くことができます。

export default class XXX {}

package.jsonを読み込みたい

コマンドラインツールなので、 --help でバージョン表示などをしたいのですが、ここは可能ならpackge.jsonで定義しているバージョン情報などを使いたいところです。
CommonJSであれば、requireでjsonを読み込むことができます。

const { version, description } = require("../package.json");

ですが、ESModuleでは上記のように読み込むことはできません😇
その場合は、requireを生成してあげることで取得することができました。

const require = createRequire(import.meta.url);
const { version, description } = require("../package.json");

CLIツールとして動作させるために

nodeのモジュールをCLIツールとして動作させるには、package.jsonのbinに実行パスを指定するのとシバンの指定をすればOKです。

package.json
{
  "bin": {
    "git-ex": "./src/index.js"
  },
}

"git-ex" の部分がコマンド名です。右辺側に実行するパスを指定します。TypeScriptなどでコンパイルしている場合はそちらを指定します。

TypeScriptでdistに出力しているような場合

package.json
{
  "bin": {
    "git-ex": "./dist/index.js"
  },
}

あとはその実行ファイルの先頭行にシバンの指定をしてあげます。

#! /usr/bin/env node

import { Option, program } from "commander";
import { RmMerged } from "./command/rmMerged.js";
// 実装がつらつら

シバンとはなんぞやという方はこちらをご覧ください。
要はそのスクリプトファイルを何で実行するかを定義してあげる必要がある感じです。

ちょっと注意点としては、webpackを使った際にこのシバンの定義が消えてしまっていて、npm installした際に動かなくなってしまいました。今回は結局そういうのは使わなかったのでいいのですが、使いたい場合は調べたほうが良さそうです(回避策はありそう?な感じでした)

ソースコードからインストール

開発中は以下のように実行できます。

node ./src/index.js

package.jsonのscriptに指定して、普通は npm start とかで実行するものかなと思うのですが、ソースコードからグローバルインストールするにはどうすればいいのでしょうか?

意外とググっても出てこなかったのですが、そのままカレントディレクトリを指定すればOKみたいです

npm install -g ./

こうするとローカルのプロジェクトをグルーバルインストールできます。npmに公開しなくてもツールのインストールができるので便利ですね。

commander.js

コマンドラインの解析ツールにはcommander.jsを使用しました。
引数のパース処理とか、オプション、サブコマンド、ヘルプの自動生成をしてくれるライブラリで、これを使えば誰でもCLIツールが作れるようになります(?)

なにか自分でCLIツールを作りたいときは、その言語でこの手のライブラリを見つけることから始めるといいと思います。だいたいどの言語にも作ってくれている人がいると思うので、活用するのがおすすめです(自分でやろうと思ったらあまりにも大変だと思う。。)

これは git-ex の一部ですが、こんな感じで引数のサブコマンドを指定してあげるだけでよしなに関数を呼び出してくれる感じです。しかも、ヘルプまで生成してくれます。

index.js
program
  .command("switch")
  .description("switch branch")
  .option("-r, --remote", "switch remote branch")
  .action((options) => switchBranch.action(options.remote));

program
  .command("stage")
  .description("stage files")
  .addOption(instructionsOption)
  .action((options) => stage.action(options.instructions));

prompts

2つ目がpromptsです。git-exの核を担っているライブラリで、ターミナルでセレクターや入力などのプロンプトのUIを提供してくれているライブラリです。
正直、このライブラリが良さそうだったので、git-exはJavaScriptで書きました。

このライブラリとても良くできていて、書きやすく、動作もばっちしでした。
ただ、作者がTypeScriptを使っていないというのもあって、TypeScriptから使おうとすると苦戦するかもです。
READMEの書き方で書けないですし、記載もないので自分でPRかコードを見て把握する必要があります。また、ESCキーなどでのキャンセルの動作もサポートされているはずなのですが、TypeScriptで記述した場合Exceptionが返ってきました(笑)

Gitの呼び出し部分

皆さんご存知Gitは、なかなか融通の効かない感じになっておりまして、git-exはGitを呼び出しては、わりとごにょごにょ工夫して実装している部分がおおいです(笑)

Gitの呼び出しには child_processexecSync()spawnSync() を使っています。
同期関数なので処理はブロックしてしまうのですが、ターミナルは止めても問題ないので特に気にせず使っています。

使い分けなのですが、Gitのコマンド実行の結果を画面に表示せず取得したいときは execSync で、Gitのコマンド結果を画面に出力したい場合は spawnSync を使用しています。

例えば、ブランチの取得ですが、実行するのは git branch です。これをspawnのほうで実行してしまうとターミナルに結果が出力されてしまうので、execのほうを使ってあげることで文字列として結果を取得することができます。

const branches = execSync("git branch").toString().split(/\n/); // 最後の空行はトリミング

今度はDiffの実行です。今度は、ユーザーが選択したファイルをもとにDiffの結果を出力してあげたいので、spawnを使ってあげます。

spawnSync("git", ["diff", ...files], { stdio: "inherit" });

spawnのほうは若干癖のある書き方の気がしますね。inheritの部分をなしで実行するとdiffの結果が色無しで出力されてしまいます。inheritをつけてあげると、そのまま git diff を実行したときと同じ結果が出力されるようになる感じです。

prettierとGithub Actions

テストとかLinterとかは書いてないですし導入もしてないのですが、フォーマッターは欲しいなと思ってprettierを入れてみました。
特にこだわりはないのでprettierはデフォルトの設定のまま入れています。

npm i -d prettier
# フォーマット実行
npx prettier --write 'src/**/*.js"

VSCodeの自動保存でフォーマットをかけるか、PR前に忘れずに実行すればいいのですが、人間忘れがちなものです。

そこでGithub Actionsで prettier --check 'src/**/*.js' を実行するようにして、PR時にパスしてなければやり直してねって形にしています。

おわり

npmに公開するCLIツールを作ったのは初めてだったのですが、かなり書きやすくていい感じでした。
npmの公開の仕方も簡単でしたし、保守もやりやすそうです。

今後別のツールを作るときもしばらくはnodeで作ろうかなと思います。
慣れておけばWebのフロントエンドやサーバーサイドも書けるのでやっぱり便利よなって思いました。

git-ex自体も機能追加していって便利にしていく予定です✨

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3