「あれ、このworktreeどこだっけ」「このブランチ、もう消していいやつ?」——git worktree list と git branch を交互に眺めて指が止まる夕方、cd 先がパッと出てこない自分に静かに苛立っている人向けの記事です。
git worktree list を打ってパスをコピーして cd する。終わったら git worktree remove、それからローカルブランチも消す——AIエージェントに並列でタスクを振るようになってから、worktreeとブランチが指数関数的に増える日常になりました(手で片付けるスピードがエージェントの作業スピードに完全に負けている)。
worktreeを5個も並べると、cd するたびに「えーっと、feat/xxx のworktreeどこだっけ」と git worktree list を見直す。マージ済みのworktreeを消し忘れて node_modules がディスクを侵食する(SSDの残量警告が来てから慌てる)。ローカルブランチが200本溜まって git branch の出力をスクロールする羽目になる。
「作るのは一瞬、片付けるのは数十分」——この非対称が放置されたまま並列開発を続けると、月末あたりに du -sh を打って絶句することになります。そこで dotfiles 側にエイリアスを4本足しました。worktree移動は w、マージ済みworktree一括削除は wrm、ローカルブランチ整理は brm と bd。fzfと gh pr view を組み合わせて、Enter連打で安全に片付くようにしてあります。
「fzfとghとghqを組み合わせる」という発想自体はよく見るけれど、実際どうなの?というところまで踏み込んで、本記事ではこの git worktree fzf エイリアスのブランチ管理パターンを、ソースコードとハマりどころ込みで解説します。
この記事で学べること
wエイリアスで worktree をブランチ名表示+ログプレビュー付き fzf で切り替える設計wrmでgh pr viewの MERGED 状態を見て worktree を安全に一括削除する方法brm/bdで git ブランチを一括削除・選択削除するときの protected branch 除外- fzf + ghq でリポジトリ間をシームレスに移動する
gエイリアス - Git操作エイリアスを Nix(home-manager)で宣言的に管理するメリット
前提:必要なツール
# macOS (Homebrew)
brew install fzf ghq gh sd eza
# Nix (home-manager)
# home.packages に追加
| ツール | 役割 |
|---|---|
| fzf | ファジーファインダー(インタラクティブ選択) |
| ghq | Git リポジトリの一括管理 |
| gh | GitHub CLI(PR の状態取得に使う) |
| sd | sed の使いやすい代替 |
| eza | プレビュー表示用(ディレクトリ一覧の整形) |
w エイリアス:worktree移動をfzfで快適にする
worktreeに移動する基本操作は git worktree list でパスを確認して cd、ですが、これが地味に手間(1日に何十回もやる作業に「地味な手間」を残しておくと、年単位では結構な時間になります)。fzfにブランチ名を出して、プレビューに git log を流すとほぼ事故りません。
w = ''cd "$(git worktree list \
| awk '{print $3, $1}' \
| sd '\[|\]' "" \
| fzf --layout=reverse \
--prompt 'WORKTREE>' \
--with-nth=1 \
--preview 'git -C {2} log --oneline -20' \
| awk '{print $2}')"''
設計のポイントは3つあります。
ブランチ名で表示・パスで cd する
git worktree list の出力は <path> <sha> [<branch>] の順です。fzfの選択画面ではブランチ名だけを見せたい(パスは長くて読みにくい)が、選択後はパスに cd したい。そこで awk '{print $3, $1}' で [branch] path の順に並べ替え、--with-nth=1 で1列目(ブランチ名)だけfzfに表示させ、選択後 awk '{print $2}' でパス側を取り出します。
sd '\[|\]' "" でブランチ名の角括弧を除去しているのも見栄え対策です。[feat/xxx] だと括弧が邪魔。
プレビューで「中身」を確認する
--preview 'git -C {2} log --oneline -20' がこのエイリアスの主役です。fzfの {2} は2列目(パス)を指すので、選択中のworktreeに対して git -C <path> log を実行できます。
WORKTREE>
> feat/payment-flow
fix/login-redirect
hotfix/cron-timezone
┌─ preview ──────────────────────────
│ a1b2c3d Add payment validation
│ e4f5g6h Refactor checkout handler
│ ...
worktree名だけだと「あれ、これどこまで進んだやつだっけ」が判別できない。直近20コミットがプレビューに出るだけで、間違ったworktreeに cd してから git log を打ち直す回数が体感ゼロになります。
ハマりどころ:fishとbash/zshでのクォート
このエイリアスはfish想定の書き方です。bash/zsh で同じことをやろうとすると、シングルクォートのネストが何重にもなって読めなくなるので、関数化してしまうのが楽。
# bash/zsh の場合は関数で
w() {
local target
target=$(git worktree list \
| awk '{print $3, $1}' \
| sd '\[|\]' "" \
| fzf --layout=reverse \
--prompt 'WORKTREE>' \
--with-nth=1 \
--preview 'git -C {2} log --oneline -20' \
| awk '{print $2}')
[ -n "$target" ] && cd "$target"
}
エイリアスにこだわらず、fzfが絡むものは関数化でいいと思っています(クォートのエスケープに人生を消費しない)。fishの ''...'' で書ければシンプルなはずが、現実はバッククォートとシングルクォートが入れ子になって読めなくなる、なんてことが多いので。
wrm エイリアス:マージ済みworktree一括削除
worktreeで一番面倒なのが「終わったやつの片付け」です。git worktree remove を1個ずつ打つのは現実的じゃない(10個並んでいる日に1個ずつ打つ気力は出ない)。かといって全部消すと「これまだPR出してないやつ...」を吹き飛ばす事故が起きる。
そこで gh pr view で PR の状態を確認しながら判定するエイリアスを書きました。判定をGitHub側に委ねれば、人間は y/N を押すだけで済みます。
wrm = ''bash -c 'git worktree prune; \
git worktree list | tail -n +2 | while read p _ b _rest; do \
b=''${b//[\[\]]/}; \
state=$(gh pr view "$b" --json state -q .state 2>/dev/null); \
if [ "$state" = "MERGED" ] || [ -z "$state" ]; then \
git worktree remove "$p" && echo "Removed: $b (''${state:-no PR})"; \
fi; \
done' ''
判定ロジックは「MERGED または PRなし」
各worktreeのブランチに対して gh pr view <branch> --json state を実行し、state フィールドだけ取り出します。状態は3パターン。
| state の値 | 判定 | 理由 |
|---|---|---|
MERGED | 削除する | PRがマージ済み = もう不要 |
OPEN | 残す | レビュー中・修正中の可能性 |
| 空(PRなし) | 削除する | ブランチ作っただけで放置されてる |
「PRなしを消す」のはちょっと攻めた挙動です。実験用ブランチや調査用worktreeは PRを作らずに残ることが多いので、ここは好みで [ "$state" = "MERGED" ] だけにする方が安全な人もいます。私は「実験ブランチは消えてもまた切り直せばいい」派なので、PRなしも巻き取り対象にしています(残しておいても「このブランチ何だっけ」になる確率の方が高い)。
git worktree prune を最初に走らせる理由
ループ前に git worktree prune を置いているのは、手動で rm -rf <worktree-path> してしまった残骸を掃除するためです。worktreeディレクトリが消えても .git/worktrees/ に管理ファイルは残るので、git worktree list に「壊れたworktree」が出続けます。prune で先に整理してから本処理に入る。
gh の認証を忘れて事故るケース
gh pr view は gh auth status で認証が通ってる前提です。CIマシンや新しいMacで wrm を打つと、全PRが「state取得失敗 → 空 → 削除対象」に倒れる可能性があります。コードでは 2>/dev/null でエラーを握り潰しているので、認証失敗時に全部消されたら笑えない(OPENのworktreeまで巻き込まれて、月曜の朝にレビュー再開できない悲劇)。
最初に手動で1回だけ gh pr view <適当なブランチ> を叩いて、ちゃんと state が返るか確認するのが安全です。
brm エイリアス:マージ済みローカルブランチ一括削除
worktreeを消した後も、ローカルブランチは別途残っているのがgitの仕様です。git branch を打って200本並ぶ画面を見ると、もう何もしたくなくなる(スクロールしている途中で当初の目的を忘れる)。
brm = ''bash -c 'git fetch --prune; \
b=$({ git branch --merged main | grep -v -E "^\*|main|master"; \
git branch -vv | grep ": gone]" | awk "{print \$1}"; } \
| sed "s/^[ ]*//" | sort -u); \
[ -z "$b" ] && echo "削除対象なし" && exit 0; \
echo "$b"; \
read -p "削除OK? (y/N): " a; \
[ "$a" = y ] && echo "$b" | xargs git branch -D' ''
削除対象は2種類のORで決める
このエイリアスは「マージ済み or リモートが消えた」を削除対象にします。
git branch --merged main— main にマージ済みのブランチgit branch -vv | grep ": gone]"— リモート追跡先が消えた(リモートでブランチ削除済み)ブランチ
両方をまとめて sort -u で重複排除。GitHub の「PRマージ後にブランチ削除」設定が効いていれば、この2条件でほぼ全部のゴミブランチが拾えます。
確認プロンプトは必須
echo "$b" で削除対象一覧を表示してから read -p で y/N 確認を入れています。ここは絶対に省略しない方がいい。
brm を最初に書いたとき、確認プロンプトを抜いて自動化したらmainの作業ブランチを巻き込んで消したことがあります(厳密には git branch -D なので reflog から復活はできますが、画面を見た瞬間の心拍数は復活しない)。一覧を目視してから Enter する2秒は、安全コストとしては無料です。
git fetch --prune の重要性
冒頭の git fetch --prune を忘れると、「リモート追跡先が消えた」判定が古いままになります。GitHubでブランチを削除しても、ローカルは git fetch --prune するまで「まだリモートにある」と思っているので、対象に挙がりません。
毎回 fetch を走らせるのが面倒なら、git config --global fetch.prune true で常時有効化してもよいです。私はエイリアス側で明示しています(明示的に書いてある方が後で読み返したとき分かる)。
bd エイリアス:fzfで選んでブランチ削除(protected branch除外・未マージ警告)
brm は一括削除ですが、「特定のブランチだけ消したい」「未マージのものを判別しながら消したい」場面もあります。そこが bd(branch delete)の出番です。
bd = ''bash -c 'p="^(main|master|dev|develop|staging|production)$"; \
b=$(git branch | grep -v "^\*" | sed "s/^ //" \
| grep -v -E "$p" \
| fzf --layout=reverse --prompt "DELETE BRANCH>" \
--preview "if git merge-base --is-ancestor {} main 2>/dev/null; then \
echo \"[MERGED] mainにマージ済み\"; \
else \
echo \"[UNMERGED] 未マージコミットあり\"; \
git log main..{} --oneline; \
fi; \
echo; git log {} --oneline -5"); \
[ -z "$b" ] && exit 0; \
if git merge-base --is-ancestor "$b" main 2>/dev/null; then \
git branch -d "$b" && echo "削除: $b"; \
else \
read -p "未マージのコミットがあります。削除? (y/N): " a; \
[ "$a" = y ] && git branch -D "$b" && echo "強制削除: $b" || echo "キャンセル"; \
fi' ''
このエイリアスには3つの安全装置を仕込んでいます。
1. protected branch を fzf候補から除外
p="^(main|master|dev|develop|staging|production)$" で、main/master/dev/develop/staging/production を選択候補から消します。間違って main を選ぶ可能性をゼロにするのが目的。fzfで候補を絞り込むキー入力を間違えても、そもそも候補に出ないので消せません。
チームの命名規則に応じて release/* なども追加しておくとよいです。
2. プレビューで MERGED / UNMERGED を判定
git merge-base --is-ancestor <branch> main
これが「ブランチが main にマージ済みか」を判定する標準的な方法です。マージ済みなら [MERGED] 表示、未マージなら [UNMERGED] と一緒に git log main..<branch> で「main にない差分コミット」を表示します。
選択前にこれが見えるだけで、「あ、これまだマージしてない作業中のやつだ」と気づける。消す前の最後の関門です(fzfに表示されただけで指が止まる、という体験を一度でもすると省略する気がなくなる)。
3. -d と -D を使い分ける
- マージ済み →
git branch -d(安全な削除、未マージなら拒否される) - 未マージ → 確認プロンプト →
git branch -D(強制削除)
-d と -D の違いは何度間違えても覚えづらい(D = Destroy と覚えています)。エイリアス側で自動的に正しい方を選ぶことで、人間は判断から解放されます——疲れている夜に判断したくない選択肢が一つ減るだけで、明日の自分が助かる。
g エイリアス:fzf + ghq でリポジトリ間移動
ここからは worktree から少し離れて、関連するエイリアスを2つ。
g = ''cd "$(ghq list --full-path \
| fzf --layout=reverse \
--preview 'eza --icons --git --time-style relative -la {1}')"''
ghq でクローンしたリポジトリを ~/ghq/github.com/<owner>/<repo>/ 配下に揃えていれば、ghq list --full-path で全リポジトリのフルパスが取れます。それを fzf に流し、プレビューに eza でディレクトリ一覧を出す。
「あのプロジェクト、リポジトリ名なんだっけ」問題を1キーで解消します(GitHub上で検索する時間 → ゼロ)。
g を打って corp でフィルタ→Enterすると corporate-site に移動、g を打って dot でフィルタ→Enterすると dotfiles に移動。worktreeを跨ぐ前にまずリポジトリを跨ぐので、g → w のコンボで「どのプロジェクトのどのworktree」にも数秒で行けます。
Nix(home-manager)で宣言的に管理するメリット
これらのエイリアスは home-manager の programs.fish.shellAbbrs(fishは略語=abbr、zsh側は programs.zsh.shellAliases)に書いて Nix で管理しています。両方が共通の定義(shellShortcuts)を取り込む構成にしておくと、シェルが違っても同じコマンドが使えます。
# common.nix で定義を一元化
{
shellShortcuts = {
w = ''cd "$(git worktree list | awk '{print $3, $1}' | ...)"'';
wrm = ''bash -c '...'';
brm = ''bash -c '...'';
bd = ''bash -c '...'';
g = ''cd "$(ghq list --full-path | fzf ...)"'';
};
}
# fish.nix では abbr として読み込む
programs.fish.shellAbbrs = common.shellShortcuts;
# zsh.nix では alias として読み込む
programs.zsh.shellAliases = common.shellShortcuts;
シェル設定を Nix で書く実践的なメリットは3つあります。
1. 新しいMacで home-manager switch 一発で復元できる
これが一番大きい。dotfilesリポジトリを git clone して home-manager switch --flake . を打てば、上記のエイリアスが全部入った状態で fish/zsh が立ち上がります。source ~/.zshrc を忘れて「動かない」と悩む時間がゼロ(新Macセットアップの夜に小一時間溶かしていた過去の自分に教えたい)。
2. 設定を壊しても1コマンドでロールバックできる
home-manager generations で過去の世代が一覧でき、home-manager switch --rollback で前の世代に戻せます。エイリアスのクォートを壊して fish が起動しなくなっても、ターミナル別タブから home-manager switch --rollback すれば即復活。
3. 複数マシンで「全く同じ環境」が保証される
仕事用Mac、検証用Mac、サーバー上のLinuxで同じエイリアスが動きます。「あれ、このマシンには wrm 入ってなかった」がない。flake.lock がバージョンを固定するので、fzf のバージョン違いで挙動が変わる事故も避けられます。
dotfiles をシェルスクリプトで管理していた時代は、複数マシンで設定が微妙にズレて「Aマシンでは動くがBマシンでは動かない」が頻発していました。同じスクリプトを git pull しているはずが、なぜかBマシンだけ古いaliasが残っているような事象が普通に起きる。Nix にしてからは、宣言的に書いた内容と実環境が一致することが保証されているので、デバッグの起点が明確になります。
Nix Flakes 全体の設計と移行の流れは Nixでdotfiles管理してみた にまとめています。エイリアス系全般の発想は fzf活用術 シェルエイリアス集 もあわせてどうぞ。
まとめ
worktree とブランチの管理は、fzfと gh、ghq を組み合わせて4本のエイリアスに集約できます。
w— worktree移動(ブランチ名表示+ログプレビュー)wrm— マージ済みworktree一括削除(gh pr viewで安全判定)brm— マージ済み・リモート削除済みローカルブランチ一括削除bd— fzf選択でブランチ削除(protected除外・未マージ警告)
並列開発でworktreeとブランチが爆発的に増える時代、消すコストを下げることは作るコストを下げることと同じくらい重要です。「片付けが手軽」だと、安心してブランチを切れる——逆に言えば、片付けが面倒だと無意識にブランチを切るのを渋るようになり、結果として並列開発の良さが死にます。
エイリアスは作業に合わせて少しずつ育てていくものなので、まずは w だけ入れて使ってみて、worktreeが増えてきたら wrm を足す、くらいの順番がおすすめです。Nixで管理しておけば、後から育てたエイリアスが全マシンに勝手に伝搬します(これが本当に楽で、一度味わうと戻れない)。



