shell で Git ブランチの整理を自動化する

Git を使った開発をやっていると、手動でブランチ管理をするのが面倒になることがあります。
筆者が困っていたのは以下の2点についてでした。

* develop ブランチを毎日最新にしたいが、毎日同じようなコマンドを実行するのが面倒。
* 古いブランチがローカルに溜まっていて、いちいち削除するのが面倒。

今までは手動でこれらの管理をしていたのですが、そろそろ自動化しようと思います。

なお、私の開発環境では WSL 環境（Ubuntu）を使っています。
幸いにも shell スクリプトが使えますので、これを使って自動化をしましょう。

# この記事で学べること
shell スクリプトの書き方のうち、以下について学べます。

* 処理を共通化する書き方 : function
* ループ処理の書き方 : for ... in
* 2つ以上のコマンドを連携させるやり方 : パイプ（|）、 xargs

# ディレクトリ構成
開発で使っているディレクトリの構成は以下のようなものです。

    /home/username/project/api
    /home/username/project/front
api, front の2つの Git リポジトリが存在しています。
それぞれのリポジトリについて、不要なブランチを削除する shell を作成してみましょう。

# Step1. develop ブランチを最新化する
試しに develop ブランチを更新する shell を考えます。

    #!/bin/sh

    cd ~/project/api

    # api の develop を最新にする。
    git checkout develop
    git pull

    cd ~/project/front

    # front の develop を最新にする。
    git checkout develop
    git pull
develop ブランチを `checkout` して `pull` するだけの簡単な処理です。

これでも動作するのですが、同じ処理をコピー＆ペーストしているのが気になります。
function を作って共通化しましょう。

## Step.1 で完成したスクリプト

    #!/bin/sh

    function refresh_repository() {
        local TARGET_DIR="$1"
        cd "TARGET_DIR"

        # develop を最新にする。
        git checkout develop
        git pull
    }

    refresh_repository ~/project/api
    refresh_repository ~/project/front
`refresh_repository` という function を作りました。第1引数 `$1` でディレクトリを受け取っています。

develop ブランチを最新化する処理としては、ひとまずこれで良いでしょう。

# Step2. 不要なブランチを削除する
続いて、不要なブランチを削除する処理を追加してみます。

先ほど作った `refresh_repository` に機能を追加しましょう。

    function refresh_repository() {
        local TARGET_DIR="$1"
        cd "TARGET_DIR"

        # develop を最新にする。
        git checkout develop
        git pull

        # リモートで削除されたブランチを削除する。
        git remote prune origin
    }
まず `git pull` コマンドに `--prune` オプションを追加します。
これはローカルに残ったリモート追跡ブランチを削除するオプションです。
これと `git remote prune` を組み合わせることで、リモートで削除されたブランチを一括削除できるようになります。

しかし、リモートでブランチが削除されないケースについては対処できていません。

develop にマージ済みのブランチのうち、 feature/... で始まるものは削除したいです。
この条件を満たすブランチを列挙するには、以下のコマンドが使えます。

    $ git branch --list --merged="develop"  "feature/*"

    > feature/1000
    > feature/1001
    > ...
`git branch -d` でこれらのブランチを削除しましょう。
先ほど列挙したブランチを別のコマンドに渡すために `xargs` を使います。

    git branch --list --merged="develop" "feature/*" | xargs git branch -d

    # xargs により、以下のようなコマンドが順番に実行される。
    # git branch -d feature/1000
    # git branch -d feature/1001 
`xargs` は、標準入力で受け取った値をコマンドに渡すコマンドです。
`git branch --list` で取得したブランチを `git branch -d` で削除できるようになりました。

これで問題ないように見えますが、1つだけちょっとした問題があります。
削除するべきブランチがないときに、以下のエラーが発生するのです。

    fatal: branch name required
これは `git branch -d` をブランチ名の指定なしで実行しているために起こります。
これを防ぐためには `xargs` に `-r` オプションを指定します。

    git branch --list ... | xargs git branch -d
    ↓
    git branch --list ... | xargs -r git branch -d
`-r` オプションを指定すると、標準出力が空の時にコマンドが実行されません。
これでエラーメッセージが出なくなります。

では完成したコマンドを `refresh_repository` に追加しましょう。

## Step. 2 で完成したスクリプト

    #!/bin/sh

    function refresh_repository() {
        local TARGET_DIR="$1"
        cd "TARGET_DIR"

        # develop を最新にする。
        git checkout develop
        git pull --prune

        # リモートで削除されたブランチを削除する。
        git remote prune origin
        # develop ブランチにマージ済みの feature ブランチを削除する。
        git branch --list --merged="develop" "feature/*" | xargs -r git branch -d
    }

    refresh_repository ~/project/api
    refresh_repository ~/project/front
# Step3. ブラッシュアップする
せっかくなので、他にも気になる点を修正してみます。

とりあえず気になるところといえば、ディレクトリの指定を個別で書いているところです。

    refresh_repository ~/project/api
    refresh_repository ~/project/front
project 配下のディレクトリをループで処理すると、きれいに書けそうです。

    #!/bin/sh

    BASE_DIR=~/project

    function refresh_repository() {
        # ... （省略）
    }

    for project in $(ls $BASE_DIR) ; do
        refresh_repository "$BASE_DIR/$project"
    done
`ls $BASE_DIR` コマンドで処理対象のディレクトリを探し、ループするよう修正しました。

さらに `refresh_repository` 本体についても改良の余地があります。
develop ブランチを変数にして、 master や他の名前のブランチも処理できるようにしましょう。

## 最終的に完成したスクリプト

    #!/bin/sh

    BASE_DIR=~/project
    BASE_BRANCH=develop

    function refresh_repository() {
        local TARGET_DIR="$1"
        cd "TARGET_DIR"

        # BASE_BRANCH がローカルに存在しないかも知れないので、先に fetch しておく。
        git fetch

        # ブランチを最新にする。
        git checkout "$BASE_BRANCH"
        git pull --prune

        # リモートで削除されたブランチを削除する。
        git remote prune origin

        # BASE_BRANCH ブランチにマージ済みの feature ブランチを削除する。
        # ただし BASE_BRANCH そのものは削除の対象外にする。（--no-contains）
        git branch --list \
            --merged="$BASE_BRANCH" \
            --no-contains="$BASE_BRANCH" \
            "feature/*" \
            | xargs -r git branch -d
    }
最後のコマンドは長くなりすぎたので、 `\` を使って途中に改行を入れました。

# まとめ
shell を使って git のブランチ管理を楽にするスクリプトを作成しました。
また、 shell について以下の書き方をご紹介しました。

* function で処理を共通化する。
* ls で取得した結果を for ... in でループ処理をする。
* xargs とパイプ「|」を使い、あるコマンドの実行結果を元に、別のコマンドを繰り返し実行する。

最初の shell を作るのは少々手間ですが、一度作ってしまえばその後の作業がぐっと楽になります。
もしかしたら shell に苦手意識を持っている方もいるかも知れませんが、この機会に是非触れてみてください。