git worktreeの操作を便利にする


# はじめに

```git worktree```コマンドはブランチを並行運用する上でかなり便利なコマンドです。
(ただし、ワーキングツリー上にブランチを切ることでライブラリをクリーンインストールする必要があるため、中には嫌いな人もいるようですね＾＾)

仕事でも結構使っているのですが、毎回シェルの履歴を探して、```git worktree add```のコマンドラインを書き換えて、ブランチの作成を行ったり、```git worktree list```でブランチの一覧を取得して、```cd```コマンドでブランチのディレクトリに移動するなどの面倒なことをしていることに今更気付きました＾＾

そこで、ブランチの作成や削除、ブランチの移動を手軽にできないかと思い、シェルスクリプトやpecoを使って、git worktreeの操作を便利にする方法について、あれこれ調べた内容を記載したいと思います。

※ そもそも、```git worktree```とは何？という方は、[ブランチの切り替えをしなくてよくなるコマンド git worktree がすごい！](https://qiita.com/shibukk/items/80430b54ecda7f36ca44)を参考にしてください。

# 前提
* zsh
    * 本記事に記載のシェルスクリプトはzshを使用します。bashで使用するには、いくつか変更が必要かと思います。
* macOS 
    * ここでは、macOSに限定します。
    * Catalina 10.15で動作検証しています。
* peco
    * 移動したいブランチのパスや削除したいブランチのパスを選択するために利用します。
* Git 
    * ```git worktree```コマンドを使用します。
    * Git 2.7.0以上を使用します。
* シェルスクリプト
    * ```git worktree```コマンドを使ったシェルスクリプトを作成します。
    * 本記事に記載のシェルスクリプトの関数の引数はバリデーションを行っていないため、実際に使用する場合はご注意ください。

# インストール

未インストールの場合は、必要に応じてインストールしてください。
これらは、HomeBrewを使ってもインストールできます。

## git

```zsh
brew install git
```

## zsh

```zsh 
brew install zsh
```

## peco

```zsh
brew install peco
```
# シェルスクリプト

ここでは、git worktreeを操作するシェルスクリプトの内容について説明します。

## ブランチのワーキングツリー用の親ディレクトリパスの指定

まずは、作成するワーキングツリーのブランチを格納するための親ディレクトリを指定します。
ここは、任意のディレクトリを指定します。
ここで指定した親ディレクトリを基準に、ブランチの作成・削除・移動などを行います。

```zsh
# ワーキングツリー用の親ディレクトリパス
GIT_WORKTREE_DIR="${HOME}/.git-worktrees/"
```

## ワーキングツリーにブランチの作成

指定の親ディレクトリパスの配下にブランチ用のディレクトリを作成するシェルスクリプトの関数です。
ディレクトリの管理をしやすくするために、親ディレクトリ配下に、<リポジトリ名>/<ブランチ名> の命名規則で作成します。
例えば、リポジトリ名が ```MyRepo``` で、ブランチ名に ```feature/hogehoge```を指定した場合は、
```${HOME}/.git-worktrees/MyRepo/feature/hogehoge```のパスにブランチのディレクトリが作成されます。

```zsh
# ワーキングツリーにブランチを作成する
#
# [使い方]
# $ gwt <リポジトリ名> <ブランチ名>
#
# [例]
# $ gwt MyRepo feature/hogehoge
function gwt() {
  git worktree add ${GIT_WORKTREE_DIR}$1/$2 -b $2
}

```

## ワーキングツリーのブランチのディレクトリに移動する

ブランチのディレクトリに移動する関数です。
```git worktree list```を呼ぶことでブランチ一覧を取得できるため、これらの一覧をpecoの標準入力に渡すことで、移動するブランチの絞り込みを行います。また、pecoでブランチのパスを選択後に cdコマンドで選択したパスのディレクトリに移動します。
この関数は直接呼び出さずに、ターミナル上で ```Ctrl+@```を押下することで呼び出しを行います。

```zsh
# pecoで選択したブランチのディレクトリに移動する
#
# [使い方]
# Ctrl + @で起動し、移動するブランチのパスを選択する。
function peco-gwt-cd () {
  local selected_dir=$(git worktree list | awk '{ print $1}' | peco --query "$LBUFFER")
  if [ -n "$selected_dir" ]; then
    BUFFER="cd ${selected_dir}"
    zle accept-line
  fi
}
zle -N peco-gwt-cd
bindkey '^@' peco-gwt-cd
```

## ワーキングツリーのブランチを削除する

ワーキングツリー上のブランチのディレクトリを削除する関数です。
ディレクトリの移動方法と同様に、```git worktree list```を呼ぶことでブランチ一覧を取得できるため、これらの一覧をpecoの標準入力に渡すことで、削除するブランチの絞り込みを行います。また、pecoでブランチのパスを選択後に gwt-rm関数を呼び出しディレクトリを削除します。
gwt-rm関数では、指定のディレクトリを削除してよいか確認するプロンプトを表示し、```y``` あるいは ```Y```が入力された場合に削除を行います。
この関数は直接呼び出さずに、ターミナル上で ```Ctrl+r```を押下することで呼び出しを行います。


```zsh
# pecoで選択したブランチを削除する。
#
# [使い方]
# Ctrl + rで起動し、pecoで削除対象のパスを選択する。
# 削除しても良い場合は、y or Yを選択する。
function peco-gwt-rm () {
  local selected_dir=$(git worktree list | awk '{ print $1}' | peco --query "$LBUFFER")
  if [ -n "$selected_dir" ]; then
    BUFFER="gwt-rm ${selected_dir}"
    zle accept-line
  fi
}
zle -N peco-gwt-rm
bindkey '^r' peco-gwt-rm

# 指定のワーキングツリー上のブランチのディレクトリを削除する。
#
# [使い方]
# $ gwt-rm <ワーキングツリー上のブランチのパス>
function gwt-rm() {
  echo "$1を削除してもよろしいですか?(y/N): "

  if read -q; then
     # 強制的にワークツリー上のブランチを削除する。
     git worktree remove -f $1
#    rm -rf $1
#    git worktree prune
  else
    echo abort
  fi
}
```

## ワーキングツリーのブランチのディレクトリを別のパスに移動する。

ワーキングツリー上に作成したブランチのディレクトリを別のパスに移動する関数です。
この関数はそこまで使用することはないと思いますが、内部的に```git worktree move```を使って、移動元のブランチのディレクトリを移動先のディレクトリに移動させます。ただし、移動先は移動元と同じリポジトリ配下に限定しています。

```zsh
# ワーキングツリーのディレクトリを別のパスに移動する
# ただし、ここでは、指定する移動元と移動先は同一のリポジトリ配下のパスに限定する。
#
# [使い方]
# $ gwt-mv <リポジトリ名> <移動元のブランチ名> <移動先のブランチ名>
#
# [例]
# $ gwt-mv MyRepo feature/hogehoge feature/hogefuga
#
# [備考]
# ・移動先のパスのディレクトリがない場合はgit worktree move が失敗するため、事前にディレクトを作成する。
# ・移動元のパスの親ディレクトはgit worktree move実行後も残ったままになることを注意する。
# ・mainのワーキングツリーの移動には対応していない。
function gwt-mv() {
  mkdir -p ${GIT_WORKTREE_DIR}$1/`dirname $3`
  git worktree move ${GIT_WORKTREE_DIR}$1/$2 ${GIT_WORKTREE_DIR}$1/$3
}
```

# .zshrcに記載のシェルスクリプト全文

上記で記載したシェルスクリプトの全文です。
使用する場合は、.zshrcに記載してください。
記載後は、シェルを再起動するか sourceコマンドなどで .zshrcの再読み込みを行ってください。

```zsh
# ワーキングツリー用のディレクトリパス
GIT_WORKTREE_DIR="${HOME}/.git-worktrees/"

# ワーキングツリーにブランチを作成する
#
# [使い方]
# $ gwt <リポジトリ> <ブランチ名>>
#
# [例]
# $ gwt MyRepo feature/hogehoge
function gwt() {
  git worktree add ${GIT_WORKTREE_DIR}$1/$2 -b $2
}

# pecoで選択したブランチのディレクトリに移動する
#
# [使い方]
# Ctrl + @で起動し、移動するブランチのパスを選択する。
function peco-gwt-cd () {
  local selected_dir=$(git worktree list | awk '{ print $1}' | peco --query "$LBUFFER")
  if [ -n "$selected_dir" ]; then
    BUFFER="cd ${selected_dir}"
    zle accept-line
  fi
}
zle -N peco-gwt-cd
bindkey '^@' peco-gwt-cd

# pecoで選択したブランチを削除する。
#
# [使い方]
# Ctrl + rで起動し、pecoで削除対象のパスを選択する。
# 削除しても良い場合は、y or Yを選択する。
function peco-gwt-rm () {
  local selected_dir=$(git worktree list | awk '{ print $1}' | peco --query "$LBUFFER")
  if [ -n "$selected_dir" ]; then
    BUFFER="gwt-rm ${selected_dir}"
    zle accept-line
  fi
}
zle -N peco-gwt-rm
bindkey '^r' peco-gwt-rm


# 指定のワーキングツリー上のブランチのディレクトリを削除する。
#
# [使い方]
# $ gwt-rm <ワーキングツリー上のブランチのパス>
function gwt-rm() {
  echo "$1を削除してもよろしいですか?(y/N): "

  if read -q; then
     # 強制的にワークツリー上のブランチを削除する。
     git worktree remove -f $1
#    rm -rf $1
#    git worktree prune
  else
    echo abort
  fi
}

# ワーキングツリーのディレクトリを別のパスに移動する
# ただし、ここでは、指定する移動元と移動先は同一のリポジトリ配下のパスに限定する。
#
# [使い方]
# $ gwt-mv <リポジトリ> <移動元のブランチ名> <移動先のブランチ名>
#
# [例]
# $ gwt-mv MyRepo feature/hogehoge feature/hogefuga
#
# [備考]
# ・移動先のパスのディレクトリがない場合はgit worktree move が失敗するため、事前にディレクトを作成する。
# ・移動元のパスの親ディレクトはgit worktree move実行後も残ったままになることを注意する。
# ・mainのワーキングツリーの移動には対応していない。
function gwt-mv() {
  mkdir -p ${GIT_WORKTREE_DIR}$1/`dirname $3`
  git worktree move ${GIT_WORKTREE_DIR}$1/$2 ${GIT_WORKTREE_DIR}$1/$3
}
```


# 使い方

## ワーキングツリーにブランチの作成

gwtコマンドでブランチの作成を行います。
コマンドの形式は次の通りです。

```zsh
$ gwt <リポジトリ名> <ブランチ名>
```


例えば、```${HOME}/work/repos```上に flutterのリポジトリを cloneをしてきて試してみます。
※ あらかじめ、```${HOME}/work/repos```のディレクトリを作成しておきます。

```zsh
$ mkdir -p ${HOME}/work/repos
$ cd ${HOME}/work/repos
$ git clone git@github.com:flutter/flutter.git
```

flutterディレクトリに移動後に、```Ctrl + @```を押下してみます。

```
$ cd flutter
```

```Ctrl + @```を押下すると、mainのワーキングツリーのみが表示されます。

```
QUERY>                                                                                             
/Users/<ユーザー名>/work/repos/flutter
```
このブランチを表示すると、現在のブランチが```master```ブランチであることが分かります。
※ この時点でmasterブランチのみが表示されます。

```
$ git branch
* master
```

つぎに、```gwt```コマンドの引数のリポジトリ名に```flutter```、ブランチ名に```feature/fugafuga```を指定し、ブランチを作成します。

```
$ gwt flutter feature/fugafuga
```

上記コマンド実行結果は次のようになります。

```zsh
$ gwt flutter feature/fugafuga
Preparing worktree (new branch 'feature/fugafuga')
HEAD is now at 2cbf126fe Add `brightness` to CupertinoNavigationBar (fixes #46216) (#47521)
```

ここで、```Ctrl + @```を押下すると、作成した```feature/fugafuga```のディレクトリのパスが表示されます。

```
QUERY>                                                                                            
/Users/<ユーザー名>/work/repos/flutter
/Users/<ユーザー名>/.git-worktrees/flutter/feature/fugafuga
```

同様に、もう一つブランチを作成してみます。
リポジトリ名に```flutter```、ブランチ名に```feature/hogehoge```を指定し、ブランチを作成します。

```
$ gwt flutter feature/hogehoge
```

上記コマンド実行結果は次のようになります。


```zsh
$ gwt flutter feature/hogehoge
Preparing worktree (new branch 'feature/hogehoge')
HEAD is now at 2cbf126fe Add `brightness` to CupertinoNavigationBar (fixes #46216) (#47521)
```


ここでまた、```Ctrl + @```を押下すると、作成した```feature/hogehoge```のディレクトリのパスが表示されます。

```
QUERY>                                                                                             
/Users/<ユーザー名>/work/repos/flutter
/Users/<ユーザー名>/.git-worktrees/flutter/feature/fugafuga
/Users/<ユーザー名>/.git-worktrees/flutter/feature/hogehoge
```
実際に ```$HOME/.git-worktrees/flutter```に移動して、tree表示してみると、
```feature/fugafuga```と```feature/hogehoge```が作成されていることを確認できます。

```
$ cd $HOME/.git-worktrees/flutter
$ tree -L 3
.
└── feature
    ├── fugafuga
    │   ├── AUTHORS
    │   ├── CODEOWNERS
    │   ├── CODE_OF_CONDUCT.md
    │   ├── CONTRIBUTING.md
    │   ├── LICENSE
    │   ├── PATENT_GRANT
    │   ├── README.md
    │   ├── analysis_options.yaml
    │   ├── bin
    │   ├── dartdoc_options.yaml
    │   ├── dev
    │   ├── examples
    │   ├── flutter_console.bat
    │   └── packages
    └── hogehoge
        ├── AUTHORS
        ├── CODEOWNERS
        ├── CODE_OF_CONDUCT.md
        ├── CONTRIBUTING.md
        ├── LICENSE
        ├── PATENT_GRANT
        ├── README.md
        ├── analysis_options.yaml
        ├── bin
        ├── dartdoc_options.yaml
        ├── dev
        ├── examples
        ├── flutter_console.bat
        └── packages

12 directories, 20 files
```

次に、```$HOME/.git-worktrees/flutter/feature/fugafuga```に移動し、ブランチを表示すると、```feature/fugafuga```にブランチが切り替わったことが分かります。

```zsh
$ cd $HOME/.git-worktrees/flutter/feature/fugafuga
$ git branch
* feature/fugafuga
+ feature/hogehoge
+ master
```
同様に、```$HOME/.git-worktrees/flutter/feature/hogehoge```に移動し、ブランチを表示すると、```feature/hogehoge```にブランチが切り替わったことが分かります。

```zsh
$ cd $HOME/.git-worktrees/flutter/feature/hogehoge
$ git branch
+ feature/fugafuga
* feature/hogehoge
+ master
```

## ワーキングツリーのブランチのディレクトリに移動する

任意のワーキングツリーのブランチに移動するために、```Ctrl + @```でコマンドを起動し、移動先のブランチのパスを選択します。
※ ```Esc```や```Ctr+C```を押下することで処理をキャンセルできます。

現在、　``` $HOME/.git-worktrees/flutter/feature/hogehoge```のディレクトリにいるとします。
この状態で、``` Ctrl+@```を押下すると、次のようにワーキングリストの一覧が表示されます。

```
QUERY>                                                                                            
/Users/<ユーザー名>/work/repos/flutter
/Users/<ユーザー名>/.git-worktrees/flutter/feature/fugafuga
/Users/<ユーザー名>/.git-worktrees/flutter/feature/hogehoge
```

この時、```/Users/<ユーザー名>/work/repos/flutter```にキーボードで移動 もしくは、```repos/flutter```のようにキーボードを入力して絞り込み、```/Users/<ユーザー名>/work/repos/flutter```を選択します。

パスを選択後、```pwd```で現在のパスを表示してみます。

```
$ pwd
/Users/<ユーザー名>/work/repos/flutter
```

ブランチを表示した場合は、materブランチにブランチが切り替わったことが分かります。

```zsh
$ git branch
+ feature/fugafuga
+ feature/hogehoge
* master
```

## ワーキングツリーのブランチを削除する

任意のワーキングツリー上のブランチを削除するために、```Ctrl + r```で起動し、pecoで削除対象のパスを選択します。
※ ```Esc```や```Ctr+C```を押下することで処理をキャンセルできます。

パスを選択後、削除してよいか聞かれるため、削除がOKの場合は、```y``` あるいは ```Y```を入力します。

それでは、```feature/hogehoge```のブランチを削除してみます。
現在、　masterブランチの```$HOME/work/repos/flutter```のディレクトリにいるとします。
※ ```$HOME/.git-worktrees/flutter/feature/hogehoge```にいる状態では、```feature/hogehoge```のブランチを削除できないため、```feature/hogehoge```以外のブランチのディレクトリにいる必要があります。

この状態で、``` Ctrl+r```を押下すると、次のようにワーキングリストの一覧が表示されます。

```
QUERY>                                                                                            
/Users/<ユーザー名>/work/repos/flutter
/Users/<ユーザー名>/.git-worktrees/flutter/feature/fugafuga
/Users/<ユーザー名>/.git-worktrees/flutter/feature/hogehoge
```

この時、ブランチの移動の時と同様にキーボードで移動 もしくは 絞り込みを行い、```/Users/<ユーザー名>/.git-worktrees/flutter/feature/hogehoge```を選択します。

パスを選択後、選択したパスを削除しても良いか確認するプロンプトが表示されます。この時に y あるいは Yを入力すると削除、それ以外(例えばn)を入力すると、処理が強制終了します。

yを選択した場合は次の通りです。

```zsh
gwt-rm /Users/<ユーザー名>/.git-worktrees/flutter/feature/hogehoge
/Users/<ユーザー名>/.git-worktrees/flutter/feature/hogehogeを削除してもよろしいですか?(y/N):
y%
```

次に、```pwd```で現在のパスを表示してみます。

```
$ pwd
/Users/<ユーザー名>/work/repos/flutter
```

masterブランチの```$HOME/work/repos/flutter```のディレクトリのままであることが分かります。
また、ブランチを表示した場合は、```feature/hogehoge```はローカルブランチに残ったままであることを確認できます。

```zsh
$ git branch
+ feature/fugafuga
  feature/hogehoge
* master
```

Ctrl+@でワーキングツリーの一覧を取得すると、```/Users/<ユーザー名>/.git-worktrees/flutter/feature/hogehoge```がリストに表示されなくなり、ワーキングツリーから削除されたことが分かります。

```zsh
QUERY>                                                                                            
/Users/<ユーザー名>/work/respo/flutter
/Users/<ユーザー名>/.git-worktrees/flutter/feature/fugafuga
```

ブランチの```feature/hogehoge```のディレクトリが削除されているかを確認するために、
親ディレクトリの```$HOME/work/repos/flutter```のディレクトリに移動し、tree表示します。

```
$ cd $HOME/.git-worktrees/flutter/
$ tree -L 3
.
└── feature
    └── fugafuga
        ├── AUTHORS
        ├── CODEOWNERS
        ├── CODE_OF_CONDUCT.md
        ├── CONTRIBUTING.md
        ├── LICENSE
        ├── PATENT_GRANT
        ├── README.md
        ├── analysis_options.yaml
        ├── bin
        ├── dartdoc_options.yaml
        ├── dev
        ├── examples
        ├── flutter_console.bat
        └── packages

6 directories, 10 files
```

tree表示を確認すると、無事に```feature/hogehoge```のブランチのディレクトリが削除されたことが分かります。

## ワーキングツリーのブランチのディレクトリを別のパスに移動する。

gwt-mvコマンドで```移動元のブランチのディレクトリ```を```移動先のブランチのディレクトリ```に移動します。
コマンドの形式は次の通りです。

```zsh
$ gwt-mv <リポジトリ名> <移動元のブランチ名> <移動先のブランチ名>
```

flutterリポジトリの```feature/fugafuga```ブランチを```develop/fugafuga```に移動してみます。
まずは、現在、masterブランチの```$HOME/work/repos/flutter```のディレクトリにいるとします。

また、```gwt-mv```コマンドを実行する前に ```Ctrl+@```を押下し、ワーキングツリーの一覧を確認しておきます。

```
QUERY>                                                                                            
/Users/<ユーザー名>/work/respo/flutter
/Users/<ユーザー名>/.git-worktrees/flutter/feature/fugafuga
```

次に、以下の```gwt-mv```コマンドを実行します。

```
$ gwt-mv flutter feature/fugafuga develop/fugafuga
```

正常に移動した場合は、コンソールに何も表示されません。

次に、```Ctrl+@```を押下し、ワーキングツリーの一覧を表示します。

```
QUERY>                                                                                            
/Users/<ユーザー名>/work/respo/flutter
/Users/<ユーザー名>/.git-worktrees/flutter/develop/fugafuga
```

ブランチの```feature/fugafuga```のパスが```develop/fugafuga```に変更されたことが分かります。

また、ブランチの```feature/fugafuga```のディレクトリが移動されているかを確認するために、
親ディレクトリの```$HOME/work/repos/flutter```のディレクトリに移動し、tree表示します。

```zsh
$ tree -L 3
.
├── develop
│   └── fugafuga
│       ├── AUTHORS
│       ├── CODEOWNERS
│       ├── CODE_OF_CONDUCT.md
│       ├── CONTRIBUTING.md
│       ├── LICENSE
│       ├── PATENT_GRANT
│       ├── README.md
│       ├── analysis_options.yaml
│       ├── bin
│       ├── dartdoc_options.yaml
│       ├── dev
│       ├── examples
│       ├── flutter_console.bat
│       └── packages
└── feature

7 directories, 10 files
```

tree表示すると、移動先の```develop/fugafuga```にディレクトリが移動したことが分かります。
また、移動元の```feature/fugafuga```のfeatureディレクトリ自体は削除されずに残ったままになっています。

次に、```$HOME/.git-worktrees/flutter/develop/fugafuga```に移動し、ブランチの状態を確認します。

```zsh
$ cd $HOME/.git-worktrees/flutter/develop/fugafuga
$ git branch
* feature/fugafuga
  feature/hogehoge
+ master
```

ディレクトリの移動後も、ローカルブランチ自体は、```feature/fugafuga```のままであることが分かります。ローカルブランチを```feature/fugafuga```から```develop/fugafuga```に切り替えたい場合は、手動で```develop/fugafuga```ブランチを切る必要があります。

```zsh
$ git checkout -b develop/fugafuga
Switched to a new branch 'develop/fugafuga'
$ git branch
* develop/fugafuga
  feature/fugafuga
  feature/hogehoge
+ master
```

上記により、ローカルブランチが```develop/fugafuga```ブランチに切り替わります。

# さいごに

作成したシェルスクリプトは荒削りですが、```git worktree```を直接操作するよりも、```git worktree```の操作を便利にできるようになったように個人的には思います。```git worktree```を活用する上での参考になれば幸いです。

# 参考サイト

* [ブランチの切り替えをしなくてよくなるコマンド git worktree がすごい！](https://qiita.com/shibukk/items/80430b54ecda7f36ca44)
* [LINE iOS開発で実践しているGit tips](https://www.slideshare.net/linecorp/line-iosgit-tips-125783296)