作業を退避させず、机をもう一つ足す
一言でいうと
git worktreeは、1つのリポジトリ(1つの.git)に作業ディレクトリを複数付ける機能です。コミット・ブランチ・オブジェクトはすべて共有し、チェックアウトした場所だけが変わります。そのため、作業中の内容をどこにも退避しないまま、別のブランチを開けます。
なぜ必要なのか
金曜日の午後に、こんなことが起きます。機能ブランチでファイル12個を半分ほど直してあるのに、本番で障害が起きて、リリースブランチにホットフィックスを入れる必要があります。普通は、次のようにします。
git stash
git switch release
# 고치고 커밋하고 올리고
git switch feature
git stash pop
この流れには、落とし穴が3つあります。1つ目は、git stashが追跡されていないファイルをデフォルトでは退避しないことです。新しく作ったファイルは、ブランチを切り替えてもその場に残り、別のブランチに紛れ込みます。2つ目は、stashが名前のないスタックなので、2、3個たまると何が何だかわからなくなり、stash popが衝突すると、その場で復旧する必要があることです。3つ目は、ブランチを切り替えた瞬間に、ビルドのアーティファクトと依存関係のディレクトリがまるごと無効になることです。大きなプロジェクトでは、この再ビルドが、ホットフィックス自体より長くかかります。
2つ目のコピーをgit cloneで作る方法もあります。そうすれば、上の3つは解決しますが、履歴をまるごともう一組ダウンロードする必要があり、2つのコピーのブランチが別々に動き始めます。片方で作ったコミットがもう片方にはないため、結局、2つの間でまた、pushとfetchを行うことになります。
git worktreeは、その中間を正確に埋めます。オブジェクトストアは1つなので履歴を再ダウンロードせず、ディレクトリは2つなので、ビルドのアーティファクトが混ざりません。
どう動くのか
git worktree add <경로> <브랜치>を実行すると、2つのものができます(プレースホルダーは、パスとブランチです)。
- 新しいディレクトリにそのブランチがチェックアウトされ、その中の
.gitは、ディレクトリではなくファイルです。内容は、gitdir: /원래저장소/.git/worktrees/<이름>の1行だけです(プレースホルダーは、元のリポジトリのパスと、名前です)。 - 元のリポジトリの
.git/worktrees/<이름>/の下に、そのワークツリーだけのHEAD・index・gitdirのような管理ファイルができます(プレースホルダーは名前です)。
ここで重要な事実が1つ出てきます。HEADとインデックスはワークツリーごとに別ですが、ブランチの参照(refs/heads/*)はリポジトリ全体で共有されます。そのため、同じブランチを2か所でチェックアウトしようとすると、gitが拒否します。
$ git worktree add /tmp/dup main
fatal: 'main' is already used by worktree at '/home/me/repo'
これは不親切なのではなく、保護です。ブランチの参照が1つなのにワーキングツリーが2つあると、片方でコミットした瞬間に、もう片方は、自分が作っていないコミットの上に乗ることになり、インデックスと実際のファイルがずれます。本当に同じコミットを2か所で見たいなら、--detachで、ブランチなしで接続します。
後始末は、3つのコマンドに分かれます。
| 状況 | コマンド | 行うこと |
|---|---|---|
| 作業が終わり、きれいに畳みます | git worktree remove <경로> |
ディレクトリと管理ファイルを一緒に削除します |
ディレクトリをすでにrm -rfしました |
git worktree prune |
残った管理ファイルだけを削除します |
| 外付けディスクのように、一時的に見えません | git worktree lock |
pruneが触れないようにロックします |
表のコードのプレースホルダーは、パスです。
rm -rfでディレクトリだけを削除すると、git worktree listに、その場所がprunableとしてずっと残ります。その状態で同じ名前で再びaddすると、「すでにある」と言われます。そして、pruneは管理ファイルだけを削除します。その場所で作ったブランチはそのまま残るため、ブランチまで整理するには、git branch -dを別に行う必要があります。
現場での姿
- リリースブランチを常に1つ付けておきます。ホットフィックスの依頼が来たら、そのディレクトリに
cdするだけで済み、機能ブランチの半分直したファイルには、手を触れません。 - レビューするとき、同僚のブランチを別の場所に付けておき、自分のブランチと並べて開いておきます。エディターのウィンドウ2つが、それぞれ別のブランチを見ていると、比較がずっと楽になります。
- 時間のかかるテストを1か所で走らせている間に、別の場所で作業を続けます。オブジェクトストアは共有しますが、インデックスは別なので、お互いを邪魔しません。
- CIランナーが、同じリポジトリを複数のブランチで同時にビルドするときも、cloneの代わりにworktreeを使います。ディスクとネットワークを、履歴1組分で節約できます。
注意すべきこともあります。ワークツリーの中でgit gcが動くとき、他のワークツリーが参照するオブジェクトを生かしておくには、gitがそれらの場所を知っている必要がありますが、rm -rfで削除してpruneをしていない場所があると、その情報が古くなります。後始末を先延ばしにしないほうが、安全です。
次のラボですること
ワークツリーを3つ付けて、同じブランチを2回開こうとして拒否されるのを、自分の目で見ます。保存していない変更をそのままにして、ホットフィックスを別にコミットし、ディレクトリをrm -rfしたときにprunableと表示されることと、pruneのあとでもブランチが残ることを確認します。最後に、ロックされたワークツリーがremoveをどう拒否するかまで確認します。
公式ドキュメントは、git-worktreeです。