新しい機能のブランチで作業している途中に、本番側の小さな不具合を直してほしいと頼まれる。これまでは、書きかけの変更をgit stashで退避し、git switch mainで切り替え、直してコミットし、元のブランチに戻ってgit stash popする、という手順を踏んでいた。そのたびにエディタは開いていたファイルを読み直し、ビルドの成果物は古いブランチの状態のまま残り、退避したことを忘れてstashが溜まっていく。小さな修正のはずが、戻ってきたときに何をしていたか思い出すところから始まるのが嫌だった。

この切り替えそのものをやめられるのがgit worktreeだ。1つのリポジトリに作業フォルダを何本でも足せて、フォルダごとに別のブランチを開いておける。修正を頼まれたら、別のフォルダに移って直すだけで済み、書きかけの作業には指一本触れない。仕組みと使い方、それに後片付けでつまずきやすい点を、手元のWindows 11に入っているGit 2.55.0で一通り試したので、その結果をまとめておく。

worktreeは作業フォルダを増やす機能で、リポジトリは1つのまま

mainから枝分かれしたブランチで開発し、マージリクエストを経てmainへ戻る流れを描いた図
mainから枝を切り、作業してmainへ戻す。worktreeを使うと、この枝ごとに別のフォルダを持てるPhoto: TheresNoTime / CC BY-SA 4.0

ふつうのクローンは、.gitフォルダ(履歴やブランチの情報が入っている本体)が1つと、ファイルを編集するための作業フォルダが1つ、という組み合わせになっている。ブランチを切り替えると、この作業フォルダの中身が丸ごと書き換わる。同じリポジトリを2つの場所で開きたければ、もう1回クローンするのがこれまでのやり方だったが、それだと履歴が2倍の容量になり、片方で作ったブランチをもう片方から見るにはpushとfetchを挟む必要がある。

worktreeは、.gitの本体は1つのまま、作業フォルダだけを追加する。最初からある作業フォルダを「メインのワークツリー」、後から足したものを「リンクされたワークツリー」と呼ぶ。追加したフォルダの直下にあるのは.gitフォルダではなく、.gitという名前の小さなテキストファイルで、中身は1行だけだった。

gitdir: C:/Users/norio/AppData/Local/Temp/wtlab/repo/.git/worktrees/repo-feature

指している先は、メインのリポジトリの.git/worktrees/の下に作られた管理用のフォルダだ。ここにそのワークツリー専用のHEAD(今どのブランチを開いているか)とindex(ステージングの状態)が置かれ、commondirというファイルには本体への相対パス../..が、gitdirというファイルには逆向きに作業フォルダ側の.gitファイルの場所が書かれている。行きと帰りの両方向にパスが記録されていることが、後で出てくる「フォルダを動かしたときに壊れる」話の原因になる。

機能としては古く、2015年7月のGit 2.5で入った。当初は追加と掃除くらいしかできず、フォルダを消すremoveと移すmoveが加わったのは2018年のGit 2.17だ。その後も、空のブランチで作業フォルダを作る--orphanがGit 2.42で、パスを相対で記録する--relative-pathsがGit 2.48で足されている。古い解説記事では、フォルダを手で消してpruneで片付ける手順が紹介されていることがあるが、今はremoveで一度に済む。なお、サブモジュールを含むリポジトリでの複数ワークツリーは、公式の説明書きでも対応が不完全とされていて、サブモジュールのあるワークツリーはmoveできない。サブモジュールを多用するリポジトリでは、慎重に試したほうがいい。

基本はadd・list・removeの3つで足りる

まずはブランチを1つ、隣のフォルダに開いてみる。リポジトリの中で次のように打つ。

git worktree add ../repo-feature feature      # 既存のブランチ feature を開く
git worktree add -b hotfix ../repo-hotfix     # 新しいブランチ hotfix を作って開く
git worktree list

listの出力は、フォルダ、開いているコミット、ブランチ名の3列になる。

C:/Users/norio/AppData/Local/Temp/wtlab/repo         008158a [main]
C:/Users/norio/AppData/Local/Temp/wtlab/repo-feature 008158a [feature]
C:/Users/norio/AppData/Local/Temp/wtlab/repo-hotfix  008158a [hotfix]

あとはcd ../repo-hotfixで移って、いつも通り編集・コミット・pushすればよい。コミットは本体の.gitに入るので、メインのフォルダに戻ってgit log hotfixと打てば、もうそこにある。pushもfetchも要らない。

ブランチ名を書かずにgit worktree add ../quickfixとフォルダだけ渡すと、フォルダ名と同じquickfixというブランチが今のコミットから作られた。もう1つ便利だったのは、手元にはまだ無く、リモートにだけあるブランチの名前を渡した場合だ。クローンしたリポジトリでgit worktree add ../clone-topic topicとすると、origin/topicを追いかけるtopicブランチが自動で作られ、branch 'topic' set up to track 'origin/topic'.と表示された。レビューを頼まれたブランチを開くときは、これがいちばん手数が少ない。

用が済んだらgit worktree remove ../repo-hotfixで消す。作業フォルダと管理用のフォルダがまとめて消え、ブランチそのものは残る。ここで、まだコミットしていない変更や追跡していないファイルがあると、contains modified or untracked files, use --force to delete itと言われて止まる。書きかけの変更をうっかり捨てない仕組みなので、--forceは中身を確かめてから付けるようにしている。

同じブランチは2か所で開けない

試していて最初に引っかかったのがこれだ。featureをrepo-featureで開いたまま、別の場所でもfeatureを開こうとすると拒まれる。

fatal: 'feature' is already used by worktree at 'C:/Users/norio/AppData/Local/Temp/wtlab/repo-feature'

git worktree addだけでなく、メインのフォルダでgit switch featureやgit checkout featureをしても同じメッセージが出た。同じブランチを2つのフォルダで同時に編集してコミットすると、片方のコミットでブランチの先端が動き、もう片方のフォルダは自分の知らないうちに古い状態になってしまう。それを防ぐための制限なので、外さないほうがいい。git branch -d featureもcannot delete branch 'feature' used by worktree at ...と断られた。ブランチを消したいときは、先にそのワークツリーを消す順番になる。

どうしても同じ中身をもう1つ見たいときは、ブランチではなくコミットを開けばよい。git worktree add --detach ../repo-view HEADとすると、ブランチに属さない状態(detached HEAD)でフォルダが作られ、制限にはかからない。古いバージョンの動きを確かめたり、2つのコミットを並べて比べたりするときに使える。コミットはできるが、そのままだとどのブランチにも残らないので、残したくなったらgit switch -c 新しい名前でブランチにしておく。

共有されるものと、フォルダごとに別のもの

複数のブランチとマージが色付きの線で描かれた、Gitのコミット履歴のグラフ表示
どのワークツリーから`git log --graph`を見ても、同じ1本の履歴が出てくるPhoto: Self / Public domain

本体が1つなので、コミット、ブランチ、タグ、リモートの設定は全部のワークツリーで共通になる。あるフォルダで作ったブランチは、別のフォルダからすぐに見えるし、どこかでgit fetchすれば全部のフォルダに反映される。一方で、今どのブランチを開いているか、ステージングの状態、それに作業フォルダに置かれたファイルそのものは、フォルダごとに独立している。

意外だったのはstashの扱いだ。stashは履歴と同じ場所に記録されるので、ワークツリーをまたいで共有される。repo-featureでgit stashしたものを、メインのフォルダでgit stash listするとstash@{0}: WIP on feature: ...とそのまま見えた。便利な場面もあるが、フォルダごとに退避場所が分かれていると思っていると、別のフォルダでgit stash popして違う作業の変更を持ってきてしまう。worktreeを使い始めてからは、そもそもstashをほとんど使わなくなったので、自分の場合は大きな問題にはならなかった。

項目ワークツリー間で補足
コミット・ブランチ・タグ共有どのフォルダで作ってもすぐに見える
リモートとfetchの結果共有1か所でfetchすれば全部に反映
stash共有別のフォルダのstashもstash listに出る
設定(.git/config)共有フォルダ別にしたいときは下の方法で
開いているブランチ(HEAD)フォルダごと同じブランチは2か所で開けない
ステージングの状態フォルダごとgit addはそのフォルダだけに効く
追跡していないファイルフォルダごとnode_modulesやビルド結果は作り直しになる

最後の行は実務でいちばん効いてくる。新しいワークツリーには、Gitが管理しているファイルしか入らない。依存パッケージやビルドの成果物、.envのような追跡対象外の設定ファイルは空っぽなので、フォルダを作るたびにインストールやビルドをやり直すことになる。依存が重いプロジェクトでは、ワークツリーを作っては消すより、main用・作業用・レビュー用のように数本を決めて使い回すほうが楽だった。

設定をワークツリーごとに変えたい場合は、git config extensions.worktreeConfig trueを一度だけ実行しておく。そのうえでgit config --worktree キー 値と書くと、値は.git/worktrees/<名前>/config.worktreeという別のファイルに入り、そのフォルダでだけ効く。手元でも、repo-featureの管理用フォルダにconfig.worktreeができ、中に指定した値だけが書かれていることを確かめた。

フォルダを手で消したときは、pruneで片付ける

Git Guiのエラーダイアログに、Not a Git repositoryと表示されている画面
作業フォルダと本体のつながりが切れると、Gitはそこをリポジトリとして認識しなくなるPhoto: Software: Junio Hamano, Linus Torvalds und andere Screenshot: PantheraLeo1359531 / GPLv2

worktreeの後片付けで多い失敗は、git worktree removeを使わず、エクスプローラーやrm -rfでフォルダを消してしまうことだ。作業フォルダは消えても、本体の.git/worktrees/の下には管理用のフォルダが残る。手元でこれをやると、git worktree listの該当行の末尾にprunable(片付けてよい)という印が付いた。--porcelain付きで見ると、理由まで書かれている。

prunable gitdir file points to non-existent location

この残骸はgit worktree pruneで消せる。いきなり消すのが不安なら、-n(実際には消さない)と-v(何をするか表示する)を付けて先に確かめる。

git worktree prune -n -v
# Removing worktrees/repo-detached: gitdir file points to non-existent location
git worktree prune -v

放っておいても、git gcが走るときに古い残骸は自動で片付けられる。ただ、残骸があるあいだは、そこで開いていたブランチが「使用中」のまま扱われ、別のフォルダで開こうとすると先ほどのalready used by worktreeで止まる。消したはずのフォルダのせいでブランチが開けないときは、まずpruneを疑うとよい。

逆に、外付けのドライブやネットワーク上のフォルダにワークツリーを置いていると、つないでいないあいだに「消えた」と判断されて片付けられてしまうおそれがある。そういうフォルダはgit worktree lock --reason "外付けSSD"で鍵をかけておく。鍵のかかったワークツリーはpruneの対象から外れ、removeしようとしてもcannot remove a locked working tree, lock reason: 外付けSSDと止まる。外すときはgit worktree unlockだ。細かい話だが、理由に日本語を書くと、list --porcelainの出力ではlocked "USB\343\203\241..."のように8進数でエスケープされた形になった。スクリプトでこの欄を読むなら、理由は英数字にしておくほうが扱いやすい。

フォルダを移したときは、moveとrepairを使い分ける

ワークツリーの場所を変えたいときは、フォルダを直接動かさずにgit worktree move ../repo-feature ../wt-featureを使う。行きと帰りの両方のパスが書き直されるので、そのまま使い続けられる。試して気づいたのは、移したあとも管理用のフォルダの名前はrepo-featureのままだということだ。listには新しい場所が出るので困ることはないが、.git/worktrees/の中を覗いたときに名前が合わず、少し戸惑った。

メインのワークツリーはmoveで動かせない。git worktree move . ../xと打つと'.' is a main working treeと断られた。メインのフォルダごと別の場所へ移したり名前を変えたりすると、リンクされたワークツリー側の.gitファイルが古い場所を指したままになる。手元でメインのフォルダの名前を変えてから、リンク側でgit statusを打つと、fatal: not a git repository: (NULL)と出て何もできなくなった。

これを直すのがgit worktree repairだ。移した後のメインのフォルダで、リンクされたワークツリーの場所を渡して実行する。

cd ../repo2
git worktree repair ../wt-feature
# repair: .git file broken: C:/Users/norio/AppData/Local/Temp/wtlab/wt-feature

これで.gitファイルが新しい場所を指すように書き直され、リンク側のgit statusも元通りになった。リンクされたワークツリーのほうを手で動かしてしまった場合も、そのフォルダの中でgit worktree repairを打てば、本体側の記録が直る。

フォルダの組をまとめて動かすことが多いなら、最初から相対パスで記録させておく手もある。git worktree add --relative-paths ../rel -b relbとすると、.gitファイルの中身がgitdir: ../repo/.git/worktrees/relのような相対パスになり、本体の設定にextensions.relativeWorktrees = trueが書き込まれた。メインとリンクの両方を含む親フォルダごと別の場所へ移してみると、絶対パスで作ったワークツリーはnot a git repositoryで壊れたのに対し、相対パスで作ったほうはそのまま動いた。ただし、片方だけを動かしたり名前を変えたりすれば、相対パスでもつながりは切れる。また、手元では相対パスのワークツリーにrepairをかけると、絶対パスに書き戻された。相対パスを使い続けたいなら、git config worktree.useRelativePaths trueを設定してからrepairするのがよさそうだ。なお、この拡張を有効にしたリポジトリは、相対パスに対応していない古いGitからは開けなくなる点にも注意が要る。

空のブランチを横に並べる、–orphanの使いどころ

もう1つ覚えておくと便利なのが--orphanだ。git worktree add --orphan -b gh-pages ../repo-pagesとすると、履歴を1つも持たない新しいブランチが、空の作業フォルダとして作られる。listではコミットの欄が0000000になり、まだ何もコミットされていないことが分かる。

静的サイトのビルド結果だけを置くgh-pagesのようなブランチは、ソースのブランチと中身がまったく違う。ふつうに切り替えると、作業フォルダの中身が総入れ替えになって落ち着かない。ワークツリーで横に並べておけば、ソース側でビルドした結果を隣のフォルダにコピーしてコミットするだけで済む。ドキュメントだけのブランチや、試作用の使い捨てのブランチを作るときにも使える。

自分の使い方と、コマンドの早見表

試した結果をふまえて、今はこういう決まりで使っている。まず、ワークツリーはリポジトリのフォルダの中ではなく隣に置き、名前はリポジトリ名-ブランチ名にそろえる。リポジトリの中に作ると、エディタの検索やファイル監視が両方のフォルダを拾ってしまい、かえって混乱した。次に、メインのフォルダはいつでもmainをきれいな状態で開いておく場所にして、作業は必ずリンクされたワークツリーで行う。急ぎの修正を頼まれたら、mainのフォルダからgit worktree add -b fix-xxx ../repo-fix-xxxで枝を切るだけで、書きかけの作業はそのまま残る。

最後に、マージが済んだらremoveしてからブランチを消す。週に1回はgit worktree listを眺めて、prunableの付いた行や、何のために作ったか思い出せないフォルダが無いかを確かめる。ワークツリーは作るのが手軽なぶん、気づくと何本も増えていて、どこで何をしていたか分からなくなるのが唯一の弱点だと感じている。

やりたいことコマンドメモ
既存のブランチを別のフォルダで開くgit worktree add ../repo-x xリモートにだけあるブランチも自動で追跡
新しいブランチを作って開くgit worktree add -b x ../repo-x起点を指定するなら末尾にコミットを足す
コミットだけを見るgit worktree add --detach ../view HEADブランチの重複制限にかからない
一覧を見るgit worktree list--porcelainで機械向けの詳しい形式
片付けるgit worktree remove ../repo-x未コミットの変更があると止まる
手で消した残骸を掃除するgit worktree prune -n -v-nを外すと実際に消す
移すgit worktree move ../a ../bメインのワークツリーは移せない
つながりを直すgit worktree repair <場所>本体かリンクのどちらかを動かした後に
消えないように守るgit worktree lock --reason "..."外付けドライブに置くとき

ブランチを切り替えるのではなく、ブランチのある場所へ移動する。考え方を1つ変えるだけで、stashの退避と復元を繰り返す手間がほとんど無くなった。リポジトリを何本もクローンしていた人ほど、試す価値があると思う。

関連記事