Claude Media
Claude Codeでリポジトリを統合しモノレポ化する — 履歴を残す手順

Claude Codeでリポジトリを統合しモノレポ化する — 履歴を残す手順

複数のGitリポジトリを履歴ごと1つに寄せる手順です。git filter-repoでパスとタグ名を揃えてからmergeする流れと、統合後にClaude Codeへ渡す設定をまとめます。

複数のリポジトリを履歴ごとモノレポに寄せる作業は、取り込む側ではなく取り込まれる側を先に書き換える、という順序で組みます。各リポジトリのクローンを git filter-repo --to-subdirectory-filter でサブディレクトリ配下に移し、タグ名に接頭辞を付けてから、モノレポ側で git merge --allow-unrelated-histories を実行します。Claude Codeは計画の下書きと検証に使い、履歴を書き換えるコマンドは人が実行する分担が安全です。

この記事では、api と web の2つのリポジトリを packages/api と packages/web に統合する例で、手順と衝突の扱いを順に示します。統合したあとにClaude Codeが迷わないよう、CLAUDE.mdと権限をどう置くかも最後に扱います。

統合に使うコマンドは何か、Claude Codeは何を担うか

履歴を保ったまま取り込むには、次の3つの道具を使います。

役割道具何をするか
パスを揃える道具git filter-repo --to-subdirectory-filter何をするかプロジェクトのルートを、指定ディレクトリの下にあるものとして書き換える
タグ名を揃える道具git filter-repo --tag-rename何をするか指定した接頭辞でタグ名を付け替える
取り込む道具git merge --allow-unrelated-histories何をするか共通の祖先を持たない履歴を1つに結ぶ

git filter-repo の公式ドキュメントは、これらの機能を「別のリポジトリと統合する準備」の用途として挙げています。サブディレクトリへの移動、タグの改名、2つのリポジトリの統合が並んでいます。--to-subdirectory-filter は --path-rename :<directory>/ と同じ意味です。

Claude Codeに任せる範囲は、計画と検証です。履歴の書き換えは取り消せない操作なので、実行そのものは手元の端末で人が行います。Claude Codeには次の3つを頼みます。

  • 統合前に、各リポジトリのタグ・ブランチ・ルート直下のファイルを一覧にする
  • 衝突しそうな名前と、モノレポ側で重なるルートファイルを洗い出す
  • 統合後に、履歴とパスが想定どおりかを読み取り専用のコマンドで確かめる

計画から入るなら、プランモードが向いています。プランモードではClaudeがファイルを読んで計画を出すだけで、承認するまで編集は起きません。claude --permission-mode plan で起動するか、セッション中に Shift+Tab で切り替えます。

前提と準備 — 必ず新しいクローンで作業する

git filter-repo は、新しいクローンでないリポジトリでの実行を中断します。ローカルにしか存在しない履歴を誤って書き換えないための安全機構です。ドキュメントが示す手順は、まずクローンを作るところから始まります。ローカルのファイルシステム上のリポジトリから複製するときは、git clone に --no-local を付けます。それ以外のフラグは、新しいクローンかどうかの判定を混乱させたり、書き換えに必要なデータを欠かせたりするため避けるよう書かれています。

--force で検査を飛ばす近道もありますが、ドキュメントはこれを不可逆な操作と位置づけています。ローカルの複製には、--force ではなく --no-local を使います。

書き換える作業用クローンは、元のリポジトリとは別の場所に作ります。作業が終われば捨てられるので、失敗してもやり直せます。統合先のモノレポは、既存のものでも空のものでも構いません。以降は既存のモノレポ mono に、2つのリポジトリを取り込む想定です。

事前調査は、書き換え前の作業用クローンで git filter-repo --analyze を実行すると進みます。これはリポジトリの履歴を分析し、過去に存在したパス、リネームの履歴、パス・ディレクトリ・拡張子ごとのオブジェクトサイズをレポートにまとめる機能です。統合するかどうかの判断材料として、大きなバイナリが混ざっていないかを見る用途に向きます。

手順1 — 各リポジトリのパスとタグ名を揃える

最初に作業用クローンを作り、そこで書き換えます。

git clone --no-local api work-api
cd work-api
git filter-repo --to-subdirectory-filter packages/api \
  --tag-rename '':'api-'

--tag-rename の <old>:<new> は、<old> で始まるタグを <new> で始まる名前に付け替えます。公式の例は foo:bar で、foo-1.2.3 が bar-1.2.3 になります。<old> と <new> のどちらも空にできるため、'':'api-' と書くとすべてのタグの先頭に api- が付きます。

手元のgit 2.50.1とgit-filter-repo(a40bce548d2c)で、両方のオプションを同時に指定して試すと、タグは v1.0.0 から api-v1.0.0 に変わり、ファイルは packages/api/ 配下に移りました。git log --stat で見ると、変更されたパスは次のとおりです。

83023c9 api: second
 packages/api/app.txt | 1 +
16cfbd5 api: first
 packages/api/README.md | 1 +
 packages/api/app.txt   | 1 +

コミットメッセージはそのままで、ファイルのパスだけが書き換わります。web も同じ形で、packages/web と web- を指定して実行します。

先に何を書き換えるべきか

ルート直下のファイルは、すべて packages/api/ の下に移ります。package.json、.gitignore、CLAUDE.md、.github/ などもです。このため、リポジトリごとのルート設定がモノレポ全体の設定と衝突することはありません。反面、CIの設定やモノレポ直下に置きたい設定ファイルは、統合後に手で戻す必要があります。ここがClaude Codeに任せやすい部分です。

手順2 — モノレポ側でmergeする

書き換えた作業用クローンを、モノレポ側でリモートとして追加し、取得してからマージします。

cd mono
git remote add api ../work-api
git fetch api --no-tags
git merge api/main --allow-unrelated-histories -m "merge api"

git merge は、共通の祖先を持たない履歴のマージを既定では拒否します。--allow-unrelated-histories は、この安全策を外して、独立して始まった2つのプロジェクトの履歴を結ぶためのオプションです。ドキュメントは、これが非常にまれな用途だとして、既定で有効にする設定変数は存在せず、今後も追加しないと書いています。

web も同じ手順で取り込みます。2回のマージの結果を git log --graph で見ると、次のような形になりました。

*   f3c8b12 merge web
|\
| * 3f5f1c5 web: second
| * 635dbc2 web: first
*   e6c9bc8 merge api
|\
| * 83023c9 api: second
| * 16cfbd5 api: first
* d3bf54f init mono

各リポジトリのコミットはそのまま枝として残り、マージコミットで束ねられています。packages/api 配下は、パスが最初から揃っているので、ファイルごとの競合は起きません。

履歴を追うときは、git log -- packages/api/app.txt でそのパスに関わるコミットだけが出ます。手元では --follow を付けても同じ2件でした。

衝突の扱い — タグとブランチは別の仕組みで起きる

名前の衝突は、タグとブランチで性質が違います。

タグは同じ名前だと上書きされず拒否される

タグは、リポジトリ全体で共通の名前空間に置かれます。api にも web にも v1.0.0 があると、2つ目を取り込む時点で衝突します。接頭辞を付けずに試した結果が次のとおりです。

 ! [rejected]        v1.0.0     -> v1.0.0  (would clobber existing tag)

モノレポにすでに v1.0.0 があるところへ、--tags で別の v1.0.0 を取得しようとすると、このように拒否されます。既存のタグを黙って上書きしないので、事故にはなりません。ただし、拒否されたタグは取り込まれずに残ります。この仕組みに頼らず、手順1の段階で --tag-rename を使って接頭辞を付けておく方が確実です。前節の手順で --no-tags を付けて取得したのは、書き換え前のタグが混ざらないようにするためでもあります。

接頭辞を付けた場合は、git fetch api --tags で api-v1.0.0 が取り込まれ、git tag の一覧にそのまま並びます。

ブランチはリモート名の下に置かれる

ブランチは、リモートごとに remotes/api/main、remotes/web/main の形で取得されます。api の release と web の release も、remotes/api/release と remotes/web/release に分かれて並びます。名前は衝突しません。

モノレポ側に残したいブランチは、必要になった時点で git switch -c などでローカルブランチを作ります。残さない場合、リモート追跡のブランチは取り込み完了後に git remote remove api で整理できます。

統合後にClaude Codeへ渡す設定

履歴が揃ったら、統合後の構成をClaude Codeが読み取れるようにします。

統合前に各リポジトリのルートに置かれていた CLAUDE.md は、手順1の書き換えで packages/api/CLAUDE.md に移っています。Claude Codeは起動時に、作業ディレクトリと全ての親ディレクトリのCLAUDE.mdを読み込み、サブディレクトリのファイルは必要になった時点で読み込みます。ルートには全パッケージ共通の規約を、パッケージ配下にはその領域固有の規約を置く、という二段の分け方です。統合した直後は、ルートにCLAUDE.mdがない状態になるので、共通部分を新しく書き起こします。

cd mono
claude --permission-mode plan

プランモードのClaudeに頼むのは、たとえば次のような依頼です。

packages/api と packages/web のCLAUDE.mdを読んで、両方に共通する規約を
ルートのCLAUDE.mdに移す案を作ってください。ファイルはまだ編集しないでください。

他のパッケージの指示を読み込ませたくないときは、claudeMdExcludesで他チームのCLAUDE.mdを除外する設定が使えます。起動場所による設定範囲の違いは、Claude Codeモノレポ設計の解説にまとめています。Turborepoを使う構成なら、turbo.jsonとCLAUDE.mdの対応も参考になります。

検証コマンドだけを許可する

統合後の検証では、履歴を読むだけのコマンドを許可しておくと、確認のたびに承認を求められません。権限ルールは .claude/settings.json に書きます。

{
  "permissions": {
    "allow": [
      "Bash(git log *)",
      "Bash(git ls-tree *)",
      "Bash(git tag *)"
    ],
    "deny": [
      "Bash(git push *)",
      "Bash(git filter-repo *)"
    ]
  }
}

ルールの書き方は、* をサブコマンドの後ろに置きます。Bash(git log *) は git log だけを許可し、Bash(git *) ならすべてのgitコマンドを許可してしまいます。評価の順はdeny、ask、allowで、最初に一致したものが結果を決めます。履歴を書き換える git filter-repo と、公開する git push をdenyに入れておくと、Claude Codeからは実行できません。

ただし、Bashのルールは、Claudeが通常出すコマンドの形に対して一致するもので、別の書き方で呼ばれた同じプログラムまでは止めません。拒否ルールはセキュリティ上の境界ではなく、うっかり実行を防ぐ安全網として扱います。

統合後の確認 — 履歴とパスを読み取り専用で見る

統合が終わったら、次の観点で結果を確かめます。Claude Codeにはコマンドの実行と出力の読み取りを任せられます。

確認することコマンド
ファイルが想定のパスにあるかコマンドgit ls-tree -r --name-only HEAD
各パッケージの履歴が残っているかコマンドgit log --oneline -- packages/api
タグが接頭辞付きで並んでいるかコマンドgit tag
取り込み元の枝が残っているかコマンドgit log --graph --oneline

手元の確認では、git ls-tree -r --name-only HEAD の出力は README.md、packages/api/README.md、packages/api/app.txt、packages/web/README.md、packages/web/app.txt の5つでした。モノレポ本体のREADMEと、各パッケージのファイルが、パスで分かれて並びます。

公開する前に — 書き換えた履歴の扱い

統合したモノレポを共有のリモートに送る前に、書き換えが与える影響を確認します。git filter-repo のドキュメントは、履歴を書き換えたあとに他の人のクローンが古い履歴を持ち続ける問題に、複数の注意を挙げています。たとえば書き換えで消えたブランチは、git push に --mirror や --prune を付けない限りサーバー側に残ります。それを誰かがマージすると、古い履歴と新しい履歴が混ざります。

今回の統合では、元のリポジトリそのものは書き換えていません。書き換えたのは作業用クローンだけです。元のリポジトリは、モノレポへ移行したあとにアーカイブして、READMEに移行先を書いておくのが分かりやすい形です。履歴の書き換えを元のリポジトリに適用する場合のpush後の掃除は、シークレットをコミットした後の対応手順でも同じ観点を扱っています。

まとめ

履歴を残す統合は、取り込む前にパスとタグ名を揃えておけば、マージ自体は競合なく終わります。ブランチはリモート名で分かれるので放っておいてよく、気をつけるのはタグの名前だけです。Claude Codeに渡す設定の本体は、ルートに共通規約を、各パッケージに固有規約を置く二段のCLAUDE.mdです。統合の直後はルートが空なので、そこから書き起こす作業が残ります。

参照した一次ソース

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

  1. code.claude.com/docs/en/common-workflows
  2. code.claude.com/docs/en/large-codebases
  3. code.claude.com/docs/en/permissions
  4. github.com/newren/git-filter-repo
  5. git-scm.com/docs/git-merge
この記事を共有:XはてブLinkedIn