TalentX Tech Blog

Tech Blog

git worktree管理CLIツールを自作した話

バックエンドエンジニアの樋口です。

git worktreeをより快適に使いたいという動機から、Go言語でworktree管理CLIツール gwm(git worktree manager) を自作しました。 この記事では、使用ライブラリの紹介・実装中につまずいたポイントとその解決策をまとめます。

👉 リポジトリ: https://github.com/arato-make/gwm

なお、現在はClaude Codeがworktreeをサポートしています(公式ドキュメント)。
ただ、「AIエージェントで作れる時代だからこそ、仕組みを理解しておくと役立つ」と感じた部分もあったため、その経験を共有できればと思います。

gwm コマンド紹介

gwm(git worktree manager)はgit worktreeの操作を快適にするCLIツールで、tmuxが動作する環境を前提としています。
以下、主な機能をコマンドごとに紹介します。

worktree の作成・切り替え・削除

worktreeのディレクトリをどこに作成しているか意識せず、worktreeの作成・切り替え・削除を実施できます。

gwm create <branch>

指定ブランチのworktreeを作成し、該当worktreeのtmuxセッションを作成してattachします。ブランチが存在しない場合はコマンド実行時のブランチから新規作成します。

gwm cd

TUIで一覧からworktreeを選択すると、tmuxセッションにattachします。該当worktreeのtmuxセッションがない場合は新規セッションを作成します。

gwm remove <branch> [--force]

TUIで一覧から選んでworktreeを削除します。--force を付けると未コミットの変更があっても削除できます。対応するtmuxセッションや実行中サービスも合わせて自動停止します。

ファイル設定管理

git管理対象外だがworktreeに展開したいファイルの追加、ファイルの形式変更(コピー、シンボリックリンク)などが発生してもhooksを利用せず容易に変更できます。

gwm config add <path> --mode copy|symlink|substitute

管理対象ファイルを登録します。substitute モードでは、MCP設定ファイルなど絶対パスを含むファイル内の絶対パスをworktreeのパスに自動置換してコピーします。

gwm config list

登録済みエントリを一覧表示します。

サービス管理

動作確認などで開発サーバーを実行する必要があるときに、他worktreeの開発サーバーの状態を意識せず起動できます。

gwm service add <name> --command "..." [--port auto|none|<number>] [--unique]

コマンドを実行するサービスを登録します。
--unique で全worktree通じて1インスタンスのみ起動するよう限定でき、他のworktreeセッションで該当サービスがすでに動作している場合は他worktreeセッションを終了してから起動します。
コマンド内で {port} プレースホルダーを使うことで、ポートを動的に指定することができます。

gwm service start <name> / gwm service stop <name>

サービスの起動・停止を行います。

gwm service attach <name>

サービスのtmuxセッションにattachしてログを確認できます。

利用ライブラリ紹介

bubbletea

TUIを構築するためのフレームワークです。Elm Architectureをベースにしており、Model / Update / View の3つを実装するだけでインタラクティブなUIを作れます。gwmの選択UI部分はこれを使っています。

lipgloss

bubbleteaと組み合わせて使うスタイリングライブラリです。色・パディング・ボーダーをCSSライクに指定できます。

開発中につまずいたこと

cd がシェルに伝搬しない

worktree移動機能を実装する上で最初にぶつかった問題です。
CLIツールから cd /path/to/worktree を実行しても、ターミナルのカレントディレクトリは変わりません。最初は「なぜ?」と思いましたが、調べるうちにシェルとプロセスの根本的な仕組みが見えてきました。

なぜ cd が効かないのか:fork/exec の話

ターミナルで gwm cd と打つと、シェルは以下の流れで処理を実行します。

  1. fork : シェル自身をコピーして子プロセスを生成する
  2. exec : 子プロセスが gwm バイナリに置き換わって実行される
  3. exit : gwm が終了し、制御が親シェルに戻る

ここでポイントになるのが、プロセスの環境(カレントディレクトリや環境変数)は親→子方向にしか受け渡せません。子プロセスが自分のワーキングディレクトリを変更しても、それは親プロセスには影響しません。
cd がシェルのビルトインコマンドとして実装されている理由もここにあります。もし cd が外部コマンドとして実行されたら、子プロセス内でディレクトリを変更するだけになり、意味をなしません。シェル自身が cd を直接処理することで、自プロセスのワーキングディレクトリを変更できるわけです。

$ which cd
cd: shell built-in command

回避策の比較

この制約を乗り越える方法はいくつかあります。

① eval を使う

eval "$(echo 'cd /path/to/worktree')"

CLIツールが cd /path/to/worktree という文字列を標準出力に吐き出し、それを eval で親シェルに解釈・実行させます。eval はシェル自身のコンテキストで動くため、cd が有効に機能します。

② シェル関数ラッパーを定義する

function gwm() {
  if [ "$1" = "cd" ]; then
    cd "$(echo '/path/to/worktree')"
  else
    command gwm "$@"
  fi
}

~/.zshrc などでシェル関数として gwm を定義することで、cd をシェル自身のコンテキストで呼べます。ユーザーから見た使い勝手は変わらず、内部的に子プロセスを経由しないため cd が機能します。

③ tmux を使う(gwm の採用方式)

tmuxはサーバー・クライアントモデルで動いており、セッションはシェルとは独立して管理されています。 そのためgwm cdが tmux に対して「このパスで新しいセッション(またはウィンドウ)を開いてくれ」と命令すれば、tmux側が最初からそのディレクトリで新しいシェルを起動することで解決できます。(目的のディレクトリを初期位置とする新しいシェルを起動する形でディレクトリ移動を実現できます)

gwmでは下記のような形で処理しています。

gwm cd を実行
  ↓
TUIで移動先のworktreeを選択
  ↓
そのworktreeに対応するtmuxセッションが
すでに存在するか確認
  ↓
┌─ 存在する → 既存セッションにattach
└─ 存在しない → そのパスを起動ディレクトリとして
                新規セッションを作成 → attach

まとめ

AIエージェントを活用することで、CLIツール未経験でもスムーズに開発を進めることができました。
一方で背景知識があることでAIへの指示の精度が上がり、より品質の高いツールが作れると感じています。

TalentXでは一緒に働く仲間を募集しており、カジュアル面談も行っていますので気になる方はぜひご応募いただければ幸いです!

i-myrefer.jp