👏

Git stashを卒業しました

に公開

Git stashについて

みなさん、git stash使ってますか?私は結構使っていて以下の用途で使うことが多いです

  • 開発中にバグの修正依頼があった時
  • 担当機能開発中にスタブを作成したけどpushしたくない時

コマンド一つ簡単に変更を退避できるのでとても重宝しております。私はstashを学ぶまでは別のテキストファイルに変更をコピペしたりしてました。
そんな便利なstashですが、最近デメリットも感じるようになりました。

  • とりあえず退避したものがどういった修正内容か忘れる
  • stashを復元しようとしても対象のブランチに変更があった場合コンフリクトが起きる

一つ目については、stash内容にコメントを付与したり、退避した修正の中身を見ることができたりしますが、「どのブランチで取得した修正なのかなどは忘れてまた新たにstashを作成する」なんてことが多々あり、使いもしないstashが複数積み上がってことが多々ありました(stashを取得した時点のコミットも確認できますが。。めんどくてさくて。。。。)
二つ目についてはそのままの意味で、復元しようとしたら別の修正を入れていたことを忘れてコンフリクトが起きるなんてこともしばしばありました

こちらの方の記事を読んで、そんなストレスから解放させてくれる手法がGit Worktree + wtpで実現できると知ったので、実際に試してみたみました。

Git Worktreeとは

WorktreeとはGitに標準で搭載されている機能ですが、簡単に言うと複数のブランチを同時にチェックアウトできる機能です
百聞は一見にしかずと言うことで実際にやってみましょう!

ディレクトリ構成

.
├── app
│   ├── concat.py
│   └── main.py
├── README.md
└── setup
    └── requirements.txt

ブランチについてはfeature/add_funcと言う機能追加ブランチにいたとしましょう

 develop
* feature/add_func
  main

開発の状態は以下の通りです

diff --git a/app/concat.py b/app/concat.py
index 1790af8..b25b326 100644
--- a/app/concat.py
+++ b/app/concat.py
@@ -16,3 +16,4 @@ def calculate_column_mean(df: pd.DataFrame, column_name: str) -> float:
     return df[column_name].mean()

 def calculate_column_sum(df: pd.DataFrame, column_name: str) -> float:
+    pass

例えばこの状態で優先度が高い機能修正やバグ修正依頼がきたとしましょう。Worktreeを利用してバグ修正用のブランチを作成したいと思います

# mainブランチからhotfix-from-mainブランチを作成する
# .git/worktrees配下に上記のブランチをチェックアウトする
git worktree add .git/worktrees/hotfix-from-main origin/main -b hotfix-from-main

上記で作成したディレクトリに移動してどのブランチにいるか確認してみましょう

$ cd .git/worktrees/hotfix-from-main
$ git branch
  develop
+ feature/add_func
* hotfix-from-main
  main

hotfix-from-mainになってますね!
この状態で開発ブランチに戻ってみます

$ cd ../../../
$ git branch
  develop
* feature/add_func
+ hotfix-from-main
  main
$ git diff
diff --git a/app/concat.py b/app/concat.py
index 1790af8..b25b326 100644
--- a/app/concat.py
+++ b/app/concat.py
@@ -16,3 +16,4 @@ def calculate_column_mean(df: pd.DataFrame, column_name: str) -> float:
     return df[column_name].mean()

 def calculate_column_sum(df: pd.DataFrame, column_name: str) -> float:
+    pass

先ほどまで作業の状態が維持されており、すぐにでも作業を再開できます
stashの場合とWorktreeを利用した場合の手順を比較してみましょう

stashの場合は以下の手順です

  1. 変更をstash 必要に応じてコメントを入れる
  2. 修正用のブランチにチェックアウト
  3. 修正後に作業ブランチをチェックアウト
  4. git stash popで修正を戻して作業再開

一方でWorktreeを利用すると以下になります

  1. 修正用のブランチをWorktreeを利用してチェックアウト
  2. 修正後に作業ブランチをに移動して作業再開

いかがでしょうか?stashと比べてシンプルになってますね!
とても便利なWorktreeですがデメリットもいくつかあります。その中でも私的にはgit管理外のファイルは移行してくれないが特に辛いと感じました。
※他にデメリットはありますが、先駆者の方が色々まとめ下さっているのでこちらでは割愛します

# 作業ディレクトリ
$ ls -a -1 .
.
..
.env # メインのディレクトリには存在している
.git
.gitignore
.venv
.wtp.yml
app
README.md
setup

# Worktreeで作成したディレクトリ
$ ls -a -1 .git/worktrees/hotfix-from-main
.
..
.git
.gitignore
app
commondir
gitdir
HEAD
index
logs
ORIG_HEAD
README.md
refs
setup

上記のように作業ディレクトリで存在した.envファイルがWorktreeで作成したディレクトリには存在しません。git管理外のファイルはWorktreeごとに用意し直す必要があります。
このような問題を解決するのがwtpとなります

wtpとは

wtpはGit WorktreeをラッピングしてくれるCLIツールです。
前述の環境セットアップ問題だけではなく、コマンドの簡略化などのメリットがあります。

まずはツールのインストール。※Mac OSの前提で進めますが他の環境については公式リポジトリを参照してください。

brew install satococoa/tap/wtp

次のコマンドでwtpの設定ファイルの雛形を生成できます。プロジェクトのルートディレクトリで実行しましょう。

wtp init

カレントディレクトリに.wtp.ymlとうファイルが作成されますので、以下のように編集します。

version: "1.0"
defaults:
  # ワークツリーが作成されるベースディレクトリ
  base_dir: ".git/worktrees"
hooks:
  post_create:
    # gitignoreされているファイルもメインのワークツリーからコピーできる
    - type: copy
      from: ".env" # コピー元はメインワークツリーからの相対パス
      to: ".env"   # コピー先は新しいワークツリーからの相対パス
    

先ほど同じように機能開発中にバグの修正依頼がきたとしましょう。wtpでは以下のように簡単にWorktreeを作成できます。

# mainブランチからhotfix-from-mainブランチをチェックアウト
$ wtp add origin/main -b hotfix-from-main
# Worktreeに存在するブランチの一覧を表示
$ wtp list
PATH             BRANCH           STATUS  HEAD
----             ------           ------  ----
@*               feature/add_func managed 4c3ae938
hotfix-from-main hotfix-from-main managed 996dffb5

Git Worktreeの時よりシンプルなコマンドで作成できました。

worktree間の移動についても以下で移動できるようですが、自分の環境だとうまく動作しませんでした。

# worktreeに移動
$ wtp cd hotfix-from-main
# メインの作業ディレクトリに移動
$ wtp cd @

それではwtpで作成したworktree配下を確認してみましょう

 $ ls -a -1 .git/worktrees/hotfix-from-main
.
..
.env
.git
.gitignore
app
commondir
gitdir
HEAD
index
logs
ORIG_HEAD
README.md
refs
setup

しっかりと.envファイルがコピーされています。

またGit Worktreeだと環境の削除とブランチの削除を別々で実行しなきゃですがwtpであればいっぺんにできます

wtp remove --with-branch hotfix-from-main

今回は利用していませんが、worktree作成時に特定のコマンドを実行することも可能です

version: "1.0"
defaults:
    .
    .
    # 新しいワークツリー内でコマンドを自動実行
    - type: command
      command: "npm install"

このようにWorktreeとwtpを利用することで別のworktreeを作成しても開発環境をそのまま維持しながら別のブランチで作業することができます。

使ってみての所感

Git Worktree単体だと煩雑なコマンドや環境構築の手間がありましが、wtpがうまくその辺りカバーできてるなと感じました。
タイトルには「stashからの卒業」と書きましたたが、正確にはstashの使い方に今一度向き合えた気がします。
stashをいくつも積み上げるのではなく、git stash pop(最新の修正を戻す)で戻せる範囲にとどめておくのがベストだと感じました。
他にも様々なメリットがあるGit Worktree + wtpですが、今回紹介した機能を使用するだけでも想定外のブランチ間の移動がかなりストレスなく行えると思うのでぜひ参考にしてみてください!

Discussion