Claude Media
Claude Codeでモノレポの一部を履歴ごと切り出す — subtree splitとfilter-repo

Claude Codeでモノレポの一部を履歴ごと切り出す — subtree splitとfilter-repo

モノレポのディレクトリをコミット履歴ごと独立リポジトリにする手順。git subtree splitとfilter-repoの違い、Claude Codeに任せる前に置く権限ルール、元リポジトリ側の置き換えまでを書きます。

モノレポの packages/billing だけを、コミット履歴をつけたまま独立したリポジトリにしたい。この作業は、Claude Codeに任せられる部分と、人が握っておく部分がはっきり分かれます。

履歴を残して切り出す道具は主に2つです。git subtree split は元のリポジトリを壊さず、切り出し用のブランチを作ります。git filter-repo --subdirectory-filter は履歴そのものを書き換えるので、使うのは新しく取り直したクローンの中に限ります。ここでは両方の手順と、Claude Codeに実行させるときの権限の置き方、切り出したあとに元リポジトリで何をコミットするかを順に書きます。モノレポの構築側はClaude CodeでTurborepoのモノレポを設定する、分割の順番の決め方はモノリスを分割する順番をPlan modeで決める手順にあります。

subtree splitとfilter-repoはどちらを選ぶか

違いは「元のリポジトリを書き換えるかどうか」に集約されます。

観点git subtree splitgit filter-repo --subdirectory-filter
元リポジトリgit subtree splitそのまま残るgit filter-repo --subdirectory-filter実行したクローンの履歴を書き換える
出力git subtree split切り出し履歴を持つ新しいブランチgit filter-repo --subdirectory-filter切り出し済みのリポジトリそのもの
取り消しgit subtree splitブランチを消せば戻るgit filter-repo --subdirectory-filter元に戻せない前提で使う
再実行git subtree split同じ履歴なら同じコミットIDになるgit filter-repo --subdirectory-filter毎回別のクローンで実行する

git subtree split の説明には、指定した <prefix> に触れたコミット(マージを含む)だけを取り出し、各コミットの中身は <prefix> がルートにある形に直される、とあります。新しい履歴は別リポジトリとしてそのまま書き出せます。同じ履歴を同じ設定で再分割すると同じコミットIDになるため、あとから新しいコミットが増えても、前回の続きに積み上がります。

git filter-repo は、元のオブジェクトからフィルタ済みの新しいオブジェクトを作り、元の履歴を消して新しい履歴だけを残す破壊的な操作です。公式の説明にも、軽い気持ちで使わないよう注意が書かれています。

迷ったら次の基準で選べます。

  • 元のモノレポをしばらく併用し、切り出し後も同期したい: git subtree split
  • 切り出しが一度きりで、新リポジトリを正とする: git filter-repo
  • 切り出し側に不要なファイルを持ち込みたくない: git filter-repo(--path を重ねて対象を絞れる)

Claude Codeに渡す前に権限を決める

履歴の書き換えを含む作業なので、実行前に権限ルールを置いておきます。ルールは .claude/settings.json に書けます。

{
  "permissions": {
    "allow": [
      "Bash(git subtree split *)"
    ],
    "ask": [
      "Bash(git filter-repo *)",
      "Bash(git rm *)"
    ],
    "deny": [
      "Bash(git push *)"
    ]
  }
}

評価の順はdeny、ask、allowで、最初に当たったものが結果を決めます。広いdenyに対して、狭いallowで例外は作れません。git push をdenyにしたので、切り出した履歴を新しいリモートに送るのは人の手で行います。

ひとつ注意があります。Bashルールは、Claudeが書いたコマンドの文字列に対して照合されます。git -C . push や git -c push.default=current push のような書き方は Bash(git push *) に当たりません。denyは「普通の書き方を止める」ものであって、境界ではありません。確実に止めたいなら、サンドボックスか、コマンド全文を検査するPreToolUseフックを足します。

もうひとつ、ask と deny は複合コマンドの中にも効きます。cd /tmp && git filter-repo ... のようにつないでも、git filter-repo の部分がaskに当たって確認が出ます。

作業ディレクトリも決めておきます。Claude Codeは、起動したディレクトリのファイルを既定でアクセス対象にします。filter-repoを使う場合は、元のリポジトリではなく、これから作る使い捨てのクローンの中で起動すると、書き換えの影響範囲がそのクローンに閉じます。

手順A: git subtree splitで切り出す

最初に、切り出し先の空のリポジトリを用意します。Gitの公式例は git init --bare を使っています。

mkdir ../billing.git && cd ../billing.git
git init --bare

元のモノレポに戻り、対象ディレクトリを --prefix に指定して分割ブランチを作ります。

git subtree split --prefix=packages/billing \
  --annotate="(split) " -b split-billing

-b を付けると、切り出した履歴を持つブランチができます。指定するブランチ名は、すでに存在しないものに限ります。成功すると、新しい履歴の先頭コミットIDが1つ表示されます。

--annotate は、新しいコミットのメッセージ先頭に文字列を足します。元のコミットとメッセージが同じで中身(パス)だけが違うため、見分けるための目印です。ただし、再分割のたびに同じ文字列を使わないと、同一の履歴が再現される保証がなくなります。チームで1つに決めておきます。

最後に、空のリポジトリへ送ります。ここは人が実行します。

git push ../billing.git split-billing:main

Git公式の例では送り先を master にしていますが、ブランチ名は新リポジトリの運用に合わせて選びます。

切り出したあとも同期するなら --rejoin

--rejoin を付けると、分割した履歴を元のプロジェクトにマージし直します。次回以降の分割は、前回の --rejoin 以降に増えた履歴だけを調べるので、大きなリポジトリで再分割が軽くなります。切り出し後もモノレポ側にディレクトリを残して同期する運用のときに向いています。一度きりの切り出しなら不要です。

手順B: filter-repoでディレクトリをルートにする

filter-repoは、新しいクローンでの実行が前提です。公式ドキュメントには「fresh clone」の安全確認があり、新しいクローンに見えないリポジトリでは実行が止まります。--force で確認を飛ばすこともできますが、履歴の書き換えは元に戻せず、既定では最後に古いオブジェクトとreflogを即座に消します。ローカルのリポジトリをクローンするときは、--force を付けるより git clone --no-local を使うほうがよい、とも書かれています。

git clone --no-local ./monorepo billing-extract
cd billing-extract
git filter-repo --subdirectory-filter packages/billing

--subdirectory-filter <directory> は、そのディレクトリに触れた履歴だけを残し、ディレクトリをプロジェクトのルートとして扱います。--path <directory>/ --path-rename <directory>/: と同じ働きです。

実行後は、origin が外れていることを確認します。--partial の説明に「originリモートの削除を無効にする」とあるので、通常の実行では外れる動きです。送り先は自分で追加します。

git remote -v
git remote add origin git@example.com:acme/billing.git
git push -u origin main

複数のディレクトリを含めてパスを変える

--path は複数回指定すると和集合になります。切り出したいのが packages/billing と共有の型定義 packages/types/billing の2か所なら、次のように重ねます。

git filter-repo \
  --path packages/billing/ \
  --path packages/types/billing/ \
  --path-rename packages/billing/:src/ \
  --path-rename packages/types/billing/:src/types/

--path-rename はパスを選ぶものではなく、選ばれたパスの名前を変えるだけです。ドキュメントにも、パスの絞り込みと名前変更を組み合わせるときの注意として書かれています。

パス変更の履歴を追えているかを確かめる

ディレクトリがかつて別の名前だった場合、現在のパスだけで切り出すと、改名前の履歴は落ちます。--subdirectory-filter も --prefix も、指定したパスに触れたコミットだけを拾うためです。

git log --follow は、ファイルが今の名前になる前の履歴までたどれますが、対象は1つのファイルだけです。ディレクトリ単位では使えません。そこで、旧パスを調べる流れは次のようになります。

  1. 切り出したいディレクトリの中の代表的なファイルに git log --follow --name-status -- <ファイル> をかけ、旧パスを見つける
  2. 旧パスがあれば、filter-repoでは --path 旧パス/ を足し、--path-rename 旧パス/: で新しいルートに合わせる
  3. 切り出し後のリポジトリで、同じファイルに git log --follow をかけ、改名前まで履歴が続くことを確かめる

この調べものはClaude Codeに向く作業です。依頼は次のような形にします。

packages/billing 配下のファイルについて、git log --follow で
改名・移動の履歴を調べ、旧パスの一覧を表にしてください。
履歴の書き換えやpushは行わないでください。

読み取りだけの依頼なので、確認も少なく済みます。出力された旧パスの一覧は、あくまで調査結果です。filter-repoのオプションに反映する前に、人が一覧とコミットを見比べます。

切り出し後の検証

切り出しが済んだら、元のリポジトリと新しいリポジトリの履歴を突き合わせます。

# 元のモノレポ側
git log --format=%s -- packages/billing > /tmp/before.txt
 
# 切り出し後のリポジトリ側
git log --format=%s > /tmp/after.txt
 
diff /tmp/before.txt /tmp/after.txt

--annotate を付けた場合は、新しいほうのメッセージ先頭に目印が付くので、その分の差は想定内です。マージコミットの扱いで差が出ることもあります。差分が出たときは、どのコミットが違うかをClaude Codeに読ませ、原因を切り分けてもらうのが早道です。

ファイルの中身も確かめます。git ls-files の件数と、新しいリポジトリのルート直下の構成が、元の packages/billing と合っていることを見ます。

元リポジトリ側の置き換えコミット

新しいリポジトリへ送り終えたら、元のモノレポからディレクトリを外します。git subtree split は元を残すので、ここは人が決めるコミットです。

git rm -r packages/billing
git commit -m "remove: packages/billing を billing リポジトリへ移動"

コミットメッセージには、新リポジトリのURLと、切り出し時点の元コミットを書いておきます。あとで「この履歴はどこへ行ったか」をたどれるのは、この1行だけだからです。上で git rm をaskにしておいたので、実行前に確認が出ます。

同時に、元の側のCLAUDE.mdも直します。packages/billing の説明が残っていると、Claude Codeが存在しないディレクトリを前提に動きます。モノレポ内の他チームのCLAUDE.mdを読み込まない設定はclaudeMdExcludesの設定手順にまとめています。

落とし穴

filter-repoを元のクローンで走らせてしまう

filter-repoは、ローカルの履歴を書き換えます。まだリモートに無いコミットがあるクローンで実行すると、それが失われます。必ず git clone --no-local で取り直したクローンで実行し、元のクローンは消さずに残します。Claude Codeに任せるときも、依頼文に「クローン先のディレクトリ以外では実行しない」と書いておきます。

切り出しの経緯を後から調べられるようにする

切り出しの理由や判断は、コミットに残らないと数か月後には分からなくなります。履歴の調べ方はClaude Codeでgit履歴から経緯を調べるにあります。切り出しの前に、どのディレクトリをなぜ分けるかを、リポジトリのドキュメントに一行でも残しておくと、後から調べる人が助かります。

まとめ

履歴を残す切り出しは、元に戻せるかどうかで道具を選びます。元を守りたいなら git subtree split、新しいクローンで一度きりなら git filter-repo --subdirectory-filter です。Claude Codeには、旧パスの調査と履歴の突き合わせを任せ、git push と履歴の書き換えは人の確認を挟む。この分担なら、権限ルール数行で線を引けます。モノレポの設計全体はClaude Codeモノレポ設計が扱っています。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/permissions
  2. git-scm.com/docs/git-subtree
  3. github.com/newren/git-filter-repo/blob/main/Documentation/git-filter-…
  4. git-scm.com/docs/git-log
この記事を共有:XはてブLinkedIn