SOTONO / ZSHCMDS

SOTONO / DOTFILES / ZSH

03 / 06

todev

移動 / 選択zsh/repo.zsh依存: fzf

ABOUT

目的のブランチへ最短距離で移動して git pull まで済ませる。

指定ブランチ(省略時は develop)の worktree があればそこへ cd、なければ git switch する。ローカルにブランチが無い場合は remote-tracking branch から作成し、それも無ければ現在の HEAD から新規作成する。最後に upstream が設定されていれば git pull で最新化する。-s を付けると worktree・ローカル・リモートの全ブランチから fzf で選択でき、引数は絞り込みクエリになる。

USAGE

todev [-s] [branch]

OPTIONS

-s
fzf でブランチを選択する。branch 引数は絞り込みクエリになる
branch
移動先ブランチ。省略時は develop

EXAMPLES

$ todev

develop へ移動して pull

$ todev main

main へ移動して pull

$ todev -s

fzf でブランチを選んでから移動

DEMO

DEMO GIF SLOT

src/assets/gifs/todev.gif を追加するとここに表示されます

SOURCE

# worktree / ローカル / リモートブランチを「ブランチ名<TAB>パス」形式で出す内部関数
# worktree があるブランチはパス付き、それ以外はパス空。同じ名前は worktree を優先する
_todev_branch_choices() {
    local repo_path="${1:-.}"

    {
        _git_worktree_choices "$repo_path"
        git -C "$repo_path" for-each-ref --format=$'%(refname:short)\t' refs/heads/
        git -C "$repo_path" for-each-ref --format='%(refname:short)' refs/remotes/ |
            grep -v '/HEAD$' |
            awk -F/ '{
                branch = $2
                for (i = 3; i <= NF; i++) branch = branch "/" $i
                if (branch != "") print branch "\t"
            }'
    } | awk -F '\t' 'NF && $1 != "" && !seen[$1]++'
}

# 指定ブランチの worktree へ移動し、最新化する。
# ブランチが別 worktree で使われている場合はそこへ移動し、
# ブランチが存在しない場合は remote-tracking branch または現在の HEAD から作成する。
# usage: todev [-s] [branch]
#   - 引数なし: develop へ移動する
#   - -s: fzf でブランチを選んでから実行する
#   - branch: そのブランチへ移動する。-s 付きなら絞り込みクエリになる
# example: todev
# example: todev main
# example: todev -s
# example: todev -s main
todev() {
    if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
        echo "このディレクトリはGitリポジトリではありません" >&2
        return 1
    fi

    local select_branch=0
    local target_branch=""

    while [[ $# -gt 0 ]]; do
        case "$1" in
            -s)
                select_branch=1
                shift
                ;;
            -*)
                echo "unknown option: $1" >&2
                echo "usage: todev [-s] [branch]" >&2
                return 1
                ;;
            *)
                if [[ -n "$target_branch" ]]; then
                    echo "usage: todev [-s] [branch]" >&2
                    return 1
                fi
                target_branch="$1"
                shift
                ;;
        esac
    done

    local repo_root
    repo_root=$(git rev-parse --show-toplevel) || return 1

    if (( select_branch )); then
        local branch_choices
        branch_choices=$(_todev_branch_choices "$repo_root") || return 1

        if [[ -n "$target_branch" ]]; then
            branch_choices=$(printf '%s\n' "$branch_choices" |
                awk -v query="$target_branch" 'index(tolower($1), tolower(query))')
        fi

        local branch_count
        branch_count=$(printf '%s\n' "$branch_choices" | sed '/^$/d' | wc -l | tr -d ' ')

        local selected
        if (( branch_count == 0 )); then
            if [[ -n "$target_branch" ]]; then
                echo "一致するブランチがありません: $target_branch" >&2
            else
                echo "ブランチが見つかりません" >&2
            fi
            return 1
        elif (( branch_count == 1 )); then
            selected="$branch_choices"
        else
            selected=$(printf '%s\n' "$branch_choices" |
                fzf --header 'Select branch' --with-nth=1 --delimiter=$'\t')
        fi

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

        target_branch=$(printf '%s\n' "$selected" | cut -f1)
    elif [[ -z "$target_branch" ]]; then
        target_branch="develop"
    fi

    local worktree_branch worktree_path target_worktree=""
    while IFS=$'\t' read -r worktree_branch worktree_path; do
        if [[ "$worktree_branch" == "$target_branch" ]]; then
            target_worktree="$worktree_path"
            break
        fi
    done <<< "$(_git_worktree_choices "$repo_root")"

    if [[ -n "$target_worktree" ]]; then
        cd "$target_worktree" || return 1
    elif git show-ref --verify --quiet "refs/heads/${target_branch}"; then
        git switch "$target_branch" || return 1
    else
        local remote_branch=""
        if git show-ref --verify --quiet "refs/remotes/origin/${target_branch}"; then
            remote_branch="origin/${target_branch}"
        else
            local -a remote_branch_candidates
            remote_branch_candidates=(${(f)"$(git for-each-ref \
                --format='%(refname:short)' "refs/remotes/*/${target_branch}")"})

            if (( ${#remote_branch_candidates[@]} == 1 )); then
                remote_branch="${remote_branch_candidates[1]}"
            elif (( ${#remote_branch_candidates[@]} > 1 )); then
                echo "${target_branch} の remote-tracking branch が複数あります:" >&2
                printf '  %s\n' "${remote_branch_candidates[@]}" >&2
                return 1
            fi
        fi

        if [[ -n "$remote_branch" ]]; then
            git switch -c "$target_branch" --track "$remote_branch" || return 1
        else
            git switch -c "$target_branch" || return 1
        fi
    fi

    if git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null 2>&1; then
        git pull
    else
        echo "${target_branch} に upstream が未設定のため git pull はスキップします。"
    fi
}
zsh/repo.zsh