gitでリモートにないローカルブランチを対話的に確認しながら削除してみた

# はじめに

gitで開発していると、リモート側ですでにマージされて削除された開発ブランチがローカルに残ってしまうことがあると思います。
定期的にブランチの整理をしたり、気付いた時に消せばいいのですが、数が溜まって億劫になってきたので削除スクリプトを作ることにしました。

# ロジック
ロジックは非常に簡単です。

1. ローカルのブランチを列挙する
2. 各ブランチがリモートにあるか確認し、なければ削除確認プロンプトを出す
3. OKなら削除

リモートになくてもローカルだけ取っておきたいブランチがあるケースもあるので、削除前に確認するようにしています。


# ソース
delete_git_branch_not_in_remote.sh

```sh

#!/bin/sh

function delete_git_branch_not_in_remote() {
  # ディレクトリ移動
  cd $1

  # レポジトリ名確認
  local tmp_path=$(git rev-parse --show-toplevel 2>/dev/null)
  if [ -n "$tmp_path" ]; then
    local repo_name=`basename $tmp_path`
    echo "repository is '$repo_name'."
  else
    echo "$1 is not git repository."
    exit 1
  fi

  # 最新のリモート情報をフェッチ
  git fetch --prune

  # ローカルブランチ（現在のブランチは除外）
  local local_branches=`git branch | grep -v '*'`
  # リモートブランチ（「origin/HEAD -> origin/master」の行を除外し、「origin/」を消す）
  local remote_branches=`git branch -r | grep -v '\->' | sed -e 's/origin\///'`
  for local_branch in $local_branches
  do
    # ローカルブランチがリモートにあるか確認
    local is_in_remote=false
    for remote_branch in $remote_branches
    do
      if [[ "$local_branch" = "$remote_branch" ]]; then
        is_in_remote=true
        break
      fi
    done

    # リモートになかったら確認プロンプトを出す
    if [[ $is_in_remote = false ]]; then
      echo "branch '$local_branch' is not in remote. delete it? [y/N]"

      exec < /dev/tty
      read ANSWER

      # 「Y」、「y」などであれば削除
      case $ANSWER in
        "Y" | "y" | "yes" | "Yes" | "YES" ) git branch -D $local_branch;;
        * ) echo "not delete branch '$local_branch'.";;
      esac
    fi
  done

  cd $OLDPWD
}

delete_git_branch_not_in_remote $1
```

# 使い方

`sh delete_git_branch_not_in_remote.sh (レポジトリのディレクトリパス)`

Macでしか試していませんが、Windowsでもcygwinなら動くはず...。
以下は実際に使った例です。

```
$ sh delete_git_branch_not_in_remote.sh (レポジトリのディレクトリパス)
repository is 'SettingFiles'.
branch 'branch_1' is not in remote. delete it? [y/N]
y
Deleted branch branch_1 (was 1bfb024).
branch 'branch_2' is not in remote. delete it? [y/N]
N
not delete branch 'branch_2'.
```

# 既存の手法を使わなかった理由
調べてみたところいくつか方法があったのですが、どれも自分の場合は使えませんでした。
理由を書いておきます。

## 「git branch -r --merged master」でマージ済みのブランチ一覧を消す方法
うちのプロジェクトはsquashマージでマージを行なっているため、この方法だと検出できませんでした。

参考: [マージ済みのローカルブランチを全て削除](https://gist.github.com/miukoba/fc3c10a25c1c675c1e97)
こちらの「squashマージされたブランチ」

## git cherryを使った方法
masterへの一時的なsquashマージコミットを作り、それがmasterに適応されているか見るらしいのですが、うちのプロジェクトはブランチを直接masterにマージしておらず、その前に別のまとめブランチにマージするので無理と思い断念。

参考: [Gitで不要なブランチを列挙する方法いろいろ](http://unarist.hatenablog.com/entry/2018/10/01/033618)

## upstreamが消滅しているブランチを消す
これはかなり有力だったのですが、自分の場合全てのブランチにupstreamの設定をしているわけではない（他の人と作業する場合のみ設定）ので、断念しました。

参考: [マージ済みのローカルブランチを全て削除](https://gist.github.com/miukoba/fc3c10a25c1c675c1e97)
こちらの「upstreamが消滅しているブランチ」

# 感想
頑張れば、シェル書かなくてもコマンドだけでできたりするのかな

# 2024/03/27追記 zsh版作りました

```zsh
function delete_git_branch_not_in_remote() {
  # ディレクトリ移動
  cd $1

  # レポジトリ名確認
  local tmp_path=$(git rev-parse --show-toplevel 2>/dev/null)
  if [ -n "$tmp_path" ]; then
    local repo_name=`basename $tmp_path`
    echo "repository is '$repo_name'."
  else
    echo "$1 is not git repository."
    return 1
  fi

  # 最新のリモート情報をフェッチ
  git fetch --prune

  # ローカルブランチ（現在のブランチは除外）
  local local_branches=( $(git branch | grep -v '*' -p) )  # リモートブランチ（「origin/HEAD -> origin/master」の行を除外し、「origin/」を消す）
  local remote_branches=( $(git branch -r | grep -v '\->' -p | sed -e 's/origin\///') )
  for local_branch in $local_branches
  do
    # ローカルブランチがリモートにあるか確認
    local is_in_remote=false
    for remote_branch in $remote_branches
    do
      if [[ "$local_branch" = "$remote_branch" ]]; then
        is_in_remote=true
        break
      fi
    done

    # リモートになかったら確認プロンプトを出す
    if [[ $is_in_remote = false ]]; then
      echo "branch '$local_branch' is not in remote. delete it? [y/N]"

      read -r ANSWER

      # 「Y」、「y」などであれば削除
      case $ANSWER in
        "Y" | "y" | "yes" | "Yes" | "YES" ) git branch -D $local_branch;;
        * ) echo "not delete branch '$local_branch'.";;
      esac
    fi
  done

  cd $OLDPWD
}

```