Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

git-worktreeでmultirepoの開発体験を向上させる

サーバーサイドエンジニアの @shuymn です。

2022年5月現在Yappliではサービスのソースコードをサービス単位でリポジトリとして分割するmultirepo*1スタイルでソースコードを管理しています*2

この記事では、複数ブランチを行き来しながら開発をするときに困ったことと、それに対処する方法として利用したgit-worktreeの紹介に加えてmultirepoでの活用例を紹介します。

実行環境について

本記事に記載しているGitコマンドは以下のバージョンで動作確認をしています。異なるバージョンの場合、動作が異なる可能性がありますので実行前に公式のリファレンスをご確認ください。

$ git --version
git version 2.36.1

困ったこと

Yappliのソフトウェアエンジニアは常にそのとき所属しているプロジェクトで担当しているタスクだけに取り組むのではなく、Yappdate Dayやインシデント対応などで一時的にそれまで取り組んでいたタスクをストップして別のタスクに取り組むことがあります。

そのような時に良く使われるGitの操作として以下の2つがあります。

  • git stash : git-stash を使う
    • git stash save <message> でstashエントリに名前を付けることができます
  • git commit -am "wip" : あとで git reset HEAD~ する前提で適当にcommitする
    • 絶対に git reset --hard HEAD~ というように --hard を付けて実行してはいけません

これらのサブコマンドも便利ではありますが、どちらも異なるブランチの作業ディレクトリ*3をローカルに同時に存在させることはできません。そのため、一方のブランチで作業しているときに、そのままターミナルマルチプレクサ*4やエディターで別ブランチの内容を開いて作業することができません。

git-worktree

では異なるブランチの作業ディレクトリをローカルに同時に存在させるためにはどうしたらよいでしょうか?

簡単に思いつくものとしては、同じリポジトリを別々のディレクトリにcloneするというものがあります。しかしそれらをまとめて管理することはGitコマンドに標準で提供されている機能の範囲では行うことができません。

そこでGit v2.5.0で追加されたgit-worktreeを使います。git-worktreeは複数の作業ディレクトリを管理するためのコマンドです。

add

remoteに存在するブランチをローカルの特定のパスにcheckoutしたい場合は以下のように add コマンドを使います。

git worktree add <path> <branch>

ローカルに既に同じ名前のブランチが存在する場合、もしくはローカルでは別名を付けているがupstreamブランチとしてremoteに同名のブランチが存在する場合*5はこのコマンドは失敗します。そのため作業途中でgit-worktreeに移動したくなった時は気合いでなんとかする必要があります。気合いの例を書いたのですが、思いのほか長くなってしまったので折りたたみます。

気合いの具体例

※ここでは派生元をmain、作業ディレクトリのブランチをdevelopとします。

パターン1: ローカルでブランチを切ってから一度もコミットしていない場合

コミット前のすべての差分をファイルに出力します。

git diff HEAD > develop.patch

mainブランチに移動します。

git switch main

developブランチを削除します。

git branch -D develop

新たなworktreeを追加してブランチ名をdevelopとします。パスは適当です。

git worktree add ~/Works/awesome-app develop

パッチファイルを新たに作成されたディレクトリに移動させます。一度もcommitしたことのないファイル*6がある場合はこのタイミングで移動させておくと良いです。

mv develop.patch ~/Works/awesome-app/

新たに作成したディレクトリに移動した後、パッチを適用します。

cd ~/Works/awesome-app
git apply develop.patch

以上です。

パターン2: ローカルでコミットしたことがある場合

パターン1と同じやりかたでコミット前の差分をパッチファイルにする(省略)。

そして、git format-patch を使ってコミットも含めたパッチをつくります。以下の例ではpatchesディレクトリにパッチファイルが出力されます。

// mainから派生させて一度もpushしたことが無い場合
git format-patch main -o patches

// remoteにある特定のブランチ(origin/develop)をcheckoutした場合
git format-patch origin/develop -o patches

mainブランチに移動し、ローカルのdevelopブランチを削除し、worktreeで再度developブランチを作成し、パッチファイルなどを移動させます(コマンドは割愛)。

この段階で「git diff で作成したパッチ」と「git format-patch で作成したパッチ」の2種類が存在しますが、この適用順は「git format-patch で作成したパッチ」→「git diffで作成したパッチ」の順番にします。後者については適用方法は前述の通りのため割愛します。前者については以下の通りです。

git am patches/*

以上です。このように、コミットしてしまったブランチを後からworktreeで復活させるのはまあまあ大変です。

remove

worktreeを追加するコマンドを紹介したので、次は削除するコマンドを紹介します。

git worktree remove <path>

もしそのworktreeでコミットしていない変更がある場合は実行に失敗しますので、安心してください。

multirepoでの活用例

次にmultirepoでの活用例を紹介します。

特定のタスクに取り組む時に、複数リポジトリを横断して開発を行うことがあります。そのような時に、自分は以下のようなディレクトリ構成となるようにしています。

branch-1
├── repo-a
├── repo-b
└── repo-c

このようなディレクトリ構成にすることで、隣り合うディレクトリにあるリポジトリのブランチが統一され、開発がしやすくなります。また、ブランチ名とリポジトリ名が分かればworktreeのパスを決定することができるので、git-branchコマンドなどと組み合わせてremoteで削除済みのブランチのworktreeを削除するということもできるようになります。

さいごに

git-worktreeについて基本のコマンド2つを紹介しました。紹介しなかったコマンドやオプションもいくつかありますので、気になった方やより発展的なユースケースを知りたい方は公式リファレンスのgit-worktreeの項目を確認してください。

Yappliでは全方面でソフトウェアエンジニアを募集中です。もしYappliに興味がありましたら是非カジュアル面談でお話しましょう!

open.talentio.com

*1:polyrepoという呼称もありますが、本記事ではmultirepoで統一します。

*2:異なるスタイルとして、1リポジトリで複数のサービスのソースコードを管理するスタイルをmonorepoがあります。

*3:Working Treeのこと

*4:tmuxなど。最近ではiTerm2などのターミナルエミュレータの機能として存在することもあります。

*5:git checkout -b branch_b origin/branch_a というようにすると作れます(remoteがoriginの場合)。

*6:Untracked Files