Blog

fzf + zshでcdせずにすぐにリポジトリを開く

image

こまっていること

  • 仕事とかいろんな理由でローカルにリポジトリのクローンが散乱している
  • 毎回、finder で開きに行ってパス確認するのがめんどくさい
  • そもそも cd しにいくのがめんどくさい
  • これが一個ならいいんだけどいろんなリポジトリで発生している

解決策

  • fzf を使って検索機能をつけながら
  • 指定ディレクトリにあるリポジトリクローンをすぐに引っ張ってこれる

前提

  • .zshrc が触れる
  • brew コマンド導入済み(Homebrew)

Fzf の導入

fzf は、あらゆる「リスト」を対象に、文字を省略しても目的のものをすばやく見つけられるインタラクティブなフィルタープログラムです。
Go で実装されているため、数十万件のファイルがあってもリアルタイムで絞り込み結果が表示されます。ターミナルで大量のリストを扱う場面全般で、検索・選択の体験が劇的に速くなります。

つまり、はやくて曖昧に仕えてとてもべんり!というわけですね。

GitHub - junegunn/fzf: :cherry_blossom: A command-line fuzzy finder · GitHub
image

brew install fzf

.zshrc に Fzf 検索ツールを組み込む

実際に僕が使っている repo というコマンドを解説します。

これは、指定したディレクトリにあるリポジトリを検索して、すぐにそこへ cd(移動)できるコマンドです。

repo — Fzf で Git リポジトリへ一発移動

image

一言で言うと

repo は、は指定ディレクトリにある Git リポジトリを
fzf で検索・選択して cd する シェル関数です。
パスを覚えなくても、プロジェクト名の一部を打つだけで
作業ディレクトリに cd してくれます。

基本的な使い方

repo              # 既定の WORK_DIR(~/Documents/_work)から選択
repo ~/Projects   # 別ディレクトリを検索起点にする

実行すると fzf が開き、リポジトリ一覧が表示されます。上下キーで選び Enter を押すと、そのディレクトリに移動します。Esc でキャンセルすると何も起きません。

内部で何をしているか

repo は次の流れで動きます。

  1. リポジトリ探索_repo_choicesWORK_DIR 以下を find し、.git ディレクトリを持つパスを収集する
  2. fzf で第 1 選択 — 表示名(WORK_DIR からの相対パス)で絞り込み、リポジトリを選ぶ
  3. ワークツリー判定 — 選んだリポジトリに git worktree が複数ある場合、第 2 の fzf でブランチ名付きのワークツリーを選ぶ
  4. cd + 履歴記録 — 決定したパスへ移動し、~/.repo_history に追記する

探索時は node_modules.cachevenv.venv などは prune(除外) されるため、巨大な依存ディレクトリを掘りに行くことはありません。

また、~/Documents/_work 外の ~/dotfiles は常に候補に含まれる ので、dotfiles リポジトリへもすぐ飛べます。

ワークツリー対応が効く場面

git worktree add で同一リポジトリの別ブランチ用チェックアウトを持っている場合、repo は自動的に 2 段階選択 になります。

第1段階: my-app          ← リポジトリ名
第2段階: feature/login   ← ブランチ名(ワークツリー)
         main

worktree が 1 つ(メインのみ)のリポジトリでは第 2 段階はスキップされ、すぐ cd します。

repo の実装

repo は汎用性を持たせるため、いくつかの関数に分かれています。

前提変数

指定ディレクトリを指定する変数です。これを使って、全体的な関数の指定先を整えます。

hoge-fuga は適当なパスにしておいてください。

export WORK_DIR="$HOME/hoge/fuga"

メイン関数

repo() {
    local work_dir="${1:-$WORK_DIR}"
    local selected_path=$(_select_repo_or_worktree "$work_dir")

    if [[ -n "$selected_path" ]]; then
        cd "$selected_path"
        echo "$selected_path" >> "$REPOHIST_FILE"
    fi
}

コア関数

# 指定リポジトリの worktree 一覧を「ブランチ名<TAB>パス」形式で出力する内部関数
_git_worktree_choices() {
    local repo_path="${1:-.}"

    git -C "$repo_path" worktree list --porcelain -z 2>/dev/null |
        perl -0ne '
            chomp;
            if ($_ eq "") {
                if (defined $path) {
                    my $label = defined $branch ? $branch : "detached HEAD";
                    $label =~ s|^refs/heads/||;
                    print "$label\t$path\n";
                }
                undef $path;
                undef $branch;
                next;
            }
            if (s/^worktree //) {
                $path = $_;
            } elsif (s/^branch //) {
                $branch = $_;
            }
            END {
                if (defined $path) {
                    my $label = defined $branch ? $branch : "detached HEAD";
                    $label =~ s|^refs/heads/||;
                    print "$label\t$path\n";
                }
            }
        '
}

# WORK_DIR 配下の Git リポジトリ候補を「表示名<TAB>絶対パス」形式で出力する内部関数
# ~/dotfiles は WORK_DIR 外でも常に候補に含める
_repo_choices() {
    local work_dir="${1:-$WORK_DIR}"

    {
        find "$work_dir" \
            -type d \( -name "node_modules" -o -name ".cache" -o -name "Library" -o -name ".Trash" -o -name "venv" -o -name ".venv" \) -prune -o \
            -name ".git" -type d -print 2>/dev/null |
            sed 's|/.git||' |
            awk -v prefix="$work_dir/" '{
                label = $0
                if (index($0, prefix) == 1) {
                    label = substr($0, length(prefix) + 1)
                }
                print label "\t" $0
            }'

        if [[ -d "$HOME/dotfiles/.git" ]]; then
            printf '~/dotfiles\t%s\n' "$HOME/dotfiles"
        fi
    } | awk -F '\t' '!seen[$2]++'
}

# fzf でリポジトリを選び、worktree が複数あればブランチも選択してパスを返す
# usage: selected_path=$(_select_repo_or_worktree [work_dir])
_select_repo_or_worktree() {
    local work_dir="${1:-$WORK_DIR}"

    # 第1段階: メインリポジトリを選択
    local selected_repo=$(_repo_choices "$work_dir" |
        fzf --header 'Select Git repository' --with-nth=1 --delimiter=$'\t')

    if [[ -z "$selected_repo" ]]; then
        return 1
    fi

    local repo_path=$(printf '%s\n' "$selected_repo" | cut -f2-)

    # 第2段階: ワークツリーがあるかチェック
    local worktree_choices=$(_git_worktree_choices "$repo_path")
    local worktree_count=$(printf '%s\n' "$worktree_choices" | sed '/^$/d' | wc -l | tr -d ' ')

    local final_path
    if [[ $worktree_count -gt 1 ]]; then
        # ワークツリーが複数ある場合、ブランチ名を表示して選択
        local selected=$(printf '%s\n' "$worktree_choices" |
            fzf --header 'Select branch' --with-nth=1 --delimiter=$'\t')

        # 選択されたパスを取得
        if [[ -n "$selected" ]]; then
            final_path=$(printf '%s\n' "$selected" | cut -f2-)
        fi
    else
        # ワークツリーがない場合はそのままメインリポジトリを使用
        final_path="$repo_path"
    fi

    if [[ -z "$final_path" ]]; then
        return 1
    fi

    echo "$final_path"
    return 0
}

カスタマイズしたくなったら

Claude や Codex にやりたいことを伝えて、
エージェントコーディングをしてもらえると、
あなたの使いやすいようにカスタマイズできると思うので、
ご自由にどうぞ。

活用例

# fzf でリポジトリを選び、cd して Claude Code を起動する
# usage: repo-claude [work_dir]
repo-claude() {
    local work_dir="${1:-$WORK_DIR}"
    local selected_path=$(_select_repo_or_worktree "$work_dir")

    if [[ -n "$selected_path" ]]; then
        cd "$selected_path"
        echo "$selected_path" >> "$REPOHIST_FILE"
        # ここに書いておけばOK
        claude
    fi
}
SNS
Follow Me