Claude Media
Claude CodeでGit LFSへ移行する — migrate importと.gitattributesの手順

Claude CodeでGit LFSへ移行する — migrate importと.gitattributesの手順

既存リポジトリの大きなファイルをGit LFSへ移すとき、履歴を書き換えるかで手順が分かれます。Claude Codeに任せる範囲と、移行後にworktreeやCIで空ポインタになる原因をまとめます。

Git LFSへの移行は、コマンドそのものより「履歴を書き換えるか」の判断で成否が決まります。git lfs migrate importは履歴を書き換え、--no-rewriteは書き換えません。Claude Codeには調査と実行までを任せ、force-pushだけは人が握る分担が安全です。

この記事では、容量の大きい順に対象を決め、.gitattributesを整え、移行後のworktreeやCIで実体が取れているかを確かめるところまでを順に扱います。

移行を考えるきっかけはGitHubのサイズ制限

GitHubは50MiBを超えるファイルをpushすると警告を出し、100MiBを超えるファイルはブロックします。この上限を超えて追跡するには、Git Large File Storage(Git LFS)を使う必要があります。ブラウザから追加できるファイルは25MiBまでです。

すでに大きなファイルを普通のコミットに入れてしまった、というリポジトリで、LFSへの切り替えが必要になります。以下は、そうしたリポジトリで履歴ごと移す前提で読んでください。

履歴を書き換えるかで3通りに分かれる

Git LFSの移行手段は、既存の履歴を残すかどうかで選びます。git lfs migrateは、importとexportのどちらでも履歴を書き換え、コミットのSHAを新しくします。例外が--no-rewriteです。

方法履歴向く場面
git lfs migrate import履歴書き換える向く場面過去のコミットに残る大きなファイルも減らしたい
git lfs migrate import --no-rewrite履歴書き換えない(新しいコミットを1つ足す)向く場面履歴は触れたくない、今後の増加だけ止めたい
git add --renormalize .履歴書き換えない向く場面Git v2.16.0以上で、migrateを使わずに済ませたい

--no-rewriteは現在のブランチにしか効かないため、他のブランチには生成されたコミットをマージする必要があります。リポジトリの肥大を過去分まで解消したいならimport、チームの履歴に手を入れられないなら--no-rewriteか--renormalizeという分け方になります。

移行の前に用意するもの

Git LFS本体が入っていて、LFSの設定が済んでいることが前提です。次のコマンドで確かめます。

git lfs version
git lfs install

git lfs installは、cleanフィルターとsmudgeフィルターをグローバルのgit設定に登録し、いまいるリポジトリにはpre-pushフックを入れます。git add --renormalizeを使う場合は、Gitがv2.16.0以上である必要があります。

手順:調べて、追跡パターンを決め、移す

ここでは履歴を書き換えるimportの流れを示します。手を動かす順に並べると次のとおりです。

手順

移行の流れ

  1. 1

    作業ツリーをきれいにする

    未コミットの変更をコミットか退避します。migrateは作業コピーの変更を上書きするか確認するためです。

  2. 2

    容量の大きい拡張子を調べる

    git lfs migrate infoで、拡張子ごとの合計サイズを出します。

  3. 3

    対象を絞って import する

    --includeに拡張子かパスを渡し、必要なブランチを指定します。

  4. 4

    結果を検証してから push する

    履歴と.gitattributesを確かめ、問題なければforce-pushします。

容量の大きい拡張子を調べる

infoは既定で、現在のブランチかつ、どのリモートにも無いコミットだけを数えます。リポジトリ全体を見るには--everythingを付けます。

git lfs migrate info --everything --top=10

出力は拡張子ごとの合計サイズと、閾値を超えるファイルの割合です。マニュアルの例では*.mp3 284 MB 1/1 files(s) 100%のような行になります。個々のファイルの大きさで足切りしたいときは--above=1MBのように指定します。

Claude Codeに調査を頼む

infoの結果を読んで追跡パターンを決める部分は、Claude Codeに任せやすい作業です。たとえば次のように頼みます。

git lfs migrate info --everything --top=10 を実行して、結果から LFS に移すべき拡張子を提案して。
ビルド成果物や生成物が混ざっていないかも、.gitignore と照らして確認して。
import は実行せず、コマンド案までにして。

最後の一文が要点です。importは履歴を書き換えるため、提案と実行を分けておくと、人が対象を確認してから進められます。

履歴ごとimportする

対象が決まったら、ブランチと拡張子を明示します。

# main ブランチの zip だけを移す
git lfs migrate import --include-ref=main --include="*.zip"
 
# ローカルの全ブランチで 100KB 超のファイルを移す
git lfs migrate import --everything --above=100Kb

--includeと--excludeを指定したimportは、.gitattributesにパターンを追記します。指定しなければ、書き換えの過程で現れた拡張子ごとに順次追記されます。--aboveは--include、--exclude、--fixupと併用できません。

--everythingを付けても更新されるのはローカルの参照だけで、refs/remotes/origin/fooのようなリモート追跡参照は動きません。結果に納得できたら、複数ブランチをまとめてgit push --allで送る手もあります。ただし書き換えた履歴の反映なので、force-pushが前提です。

push前に結果を確かめる

importは新旧のコミットSHAの対応を残せます。--object-mapにパスを渡すと、OLD-SHA,NEW-SHA形式のCSVが書き出されます。--verboseを付ければ、移したファイルのコミットIDとファイル名が標準出力に出ます。

git lfs migrate import --include-ref=main --include="*.zip" \
  --object-map=../lfs-object-map.csv --verbose

Claude Codeには、この出力とgit diffで.gitattributesの変更を読ませ、想定外の拡張子が混ざっていないかを確かめさせられます。対象が多いときは、先に--include-refでブランチを絞ると見通しが良くなります。確認が済むまでpushしない、とCLAUDE.mdに書いたルールをここで効かせます。

force-pushの後に共同作業者がすること

importしたブランチをforce-pushすると、リモートの履歴は新しいSHAに入れ替わります。それより前に作られた共同作業者のcloneは、古いSHAの履歴を持ったままです。そこから作業を続けてpushすると、バイナリが普通のコミットに入った古い履歴がリモートに戻ってきます。

共同作業者には、force-pushの前に連絡して、次のどちらかを頼みます。

  • 未pushの作業を退避したうえで、リポジトリを取り直す(再clone)
  • 既存のcloneでgit fetchのあと、git reset --hard origin/<ブランチ名>で新しい履歴に揃える(手元の未pushのコミットは消えるので、先に別ブランチに逃がしておく)

古いブランチから切った作業ブランチも、同じ理由で書き換え前の履歴を引きずります。新しいブランチから切り直し、変更はgit cherry-pickで移す形が安全です。なお、--no-rewriteならSHAは変わらないので、この手間は要りません。

履歴を書き換えない場合

先に.gitattributesを確定させてから、対象ファイルを新しいコミットに入れます。

git lfs track "*.zip" "*.mp3" "*.psd"
git add .gitattributes
git commit -m "add Git LFS attributes"
git lfs migrate import --no-rewrite --yes \
  -m "Import zip, mp3, psd files" \
  test.zip audios/*.mp3 images/*.psd

--no-rewriteではファイルを引数で渡します。*.mp3のようなパターンを引用符で囲むと対応しないため、シェルに展開させます。git lfs trackは既存ファイルの更新時刻を変えるので、作業ツリーが変更ありに見えます。--yesはその確認を省くための指定です。

Git v2.16.0以上なら、git add --renormalize .でも同じ結果になります。trackと.gitattributesのコミットまでは同じで、その後にrenormalizeを実行して新しいコミットにします。

.gitattributesを追従させる

.gitattributesに書くパターンの決め方は、git lfs trackのマニュアルが基準になります。パターンはgitignoreと同じ規則で解釈され、*、?、[、]は特別な意味を持ちます。これらを含むファイル名をそのまま追跡したいときは--filenameを使います。

注意点は2つあります。

  • migrateの--includeは、.gitattributesと同等の書き方です。ディレクトリはpath/**の形で再帰指定します。git lfs ls-filesなど他のコマンドはgitignore形式で、同じgit lfsでもパターンの書き方が違います
  • importの後に、作業コピーのファイルがポインタに置き換わることがあります。これは正常な動作で、git lfs checkoutで実体を戻せます

.gitattributesが漏れたままimportをした場合は、--fixupが使えます。既存の.gitattributesを読み、LFSで追跡すべきなのにまだポインタでないファイルだけを変換します。--includeや--excludeとは併用できません。

Claude Codeに規約を覚えさせる

移行後、Claude Codeが大きなバイナリをふつうのコミットに入れてしまうと、追跡漏れが再発します。リポジトリのCLAUDE.mdに、運用の決まりを短く書いておく例です。

## Git LFS
- 追跡対象は .gitattributes を正とする。新しい大きなバイナリを追加する前に `git lfs track` で確認する
- `git lfs migrate import` と force-push は人が実行する。コマンド案の提示までにとどめる
- worktree やCIで実体が空に見えたら `git lfs pull` を試す

実行そのものを抑えたいなら、権限の設定でgit pushをdenyにしておけます。

{
  "permissions": {
    "deny": ["Bash(git push *)"]
  }
}

ルールは書かれたコマンドの形にしか一致しません。権限の公式ページは、git -C . push origin mainのような書き方はBash(git push *)に一致しない例として挙げています。denyは安全網であって、唯一の防御ではありません。

移行後に実体が空ポインタになる場面

移行が済んでも、場所によってはLFSの実体が取れず、中身がポインタのままになります。原因は場面ごとに違います。

場面起きること確認と対処
Claude Codeが作ったworktree起きることgit lfs install --localで設定していると、LFS管理ファイルがポインタになる確認と対処worktree内でgit lfs pullを実行する
GitHub ActionsのCI起きることactions/checkoutのlfs入力が既定でfalseのため、実体を取らない確認と対処lfs: trueを指定する
--skip-smudgeで設定した環境起きることclone・pullで自動ダウンロードされない確認と対処チェックアウトのたびにgit lfs pullが要る

Claude Codeのworktree

git lfs install --localを使っている場合、worktree作成時にLFSのフィルタが働かず、ポインタファイルが入ります。--localは設定をリポジトリの.git/configに書くため、グローバル設定を書く通常のgit lfs installは影響を受けません。リポジトリ自身のフィルタードライバーはシェルコマンドであり、Claudeを含む書き込み可能な側が仕込めるため、Claude Codeはworktreeの作成時にLFSのフィルターを実行しません。v2.1.247より前は実行されていました。

対処はgit lfs pullです。移行直後は、まずworktreeの中で実行します。

git lfs pull

git lfs pullはgit lfs fetchとgit lfs checkoutの組み合わせにあたり、現在のrefのLFSオブジェクトを取得して作業コピーに反映します。すでにローカルに実体がある場合は、ネットワーク取得なしのgit lfs checkoutだけでポインタが実体に置き換わります。変更済みのファイルは上書きされません。

ごくまれに、worktreeそのものが作られないケースがあります。リポジトリの設定がlfs.customtransfer.<name>.pathやlfs.standalonetransferagentのような、LFSにプログラムを実行させるキーを持つときです。自分で設定したものならグローバルのgit設定へ移します。身に覚えが無ければ、信頼できないツールが書いた可能性があるので、リポジトリの設定から削除します。

worktreeの基本はworktree完全ガイドにまとめています。

GitHub ActionsのCI

actions/checkoutにはlfsという入力があり、「Git-LFSファイルをダウンロードするか」を決めます。既定はfalseです。LFSを使うリポジトリでテストやビルドを回すなら、明示します。

- uses: actions/checkout@v6
  with:
    lfs: true

Claude Code Actionを使うワークフローでも、actions/checkoutでリポジトリを取得する構造は同じです。LFSの実体が必要な作業を任せるなら、この指定が漏れていないかを先に確認します。

移行を戻すとき

LFSをやめたくなった場合の逆操作がexportです。--includeを最低1つ指定し、手元に無いオブジェクトはorigin(または--remoteで指定したリモート)から取得して通常のファイルに戻します。

git lfs migrate export --everything --include="*.zip" --remote=origin

exportも履歴を書き換えるため、force-pushが必要です。また.gitattributesのエントリは消されず、git lfs untrackに似た「追跡しない」エントリが加わります。

まとめ

履歴を書き換えるimportは、過去のコミットに残る大きなファイルまで減らせる代わりに、全員の履歴が新しいSHAに変わります。そこで先にinfoで対象を絞り、--include-refで範囲を限り、結果を読んでからforce-pushする順番を守ります。履歴に手を入れられないなら--no-rewriteで足りる場面も多くあります。

移行の成否は、importが終わった時点ではなく、worktreeとCIのそれぞれで実体が取れた時点で決まります。LFS管理のはずのファイルが実体でなくポインタのままだと気づいたら、まずgit lfs pull、CIならlfs: trueを確認してください。Claude Codeで履歴の調査から入る場合はgit履歴から経緯を調べる方法も参考になります。

参照した一次ソース

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

  1. github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-migrate.adoc
  2. github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-track.adoc
  3. github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-checkout.adoc
  4. code.claude.com/docs/en/worktrees
  5. code.claude.com/docs/en/permissions
  6. code.claude.com/docs/en/common-workflows
  7. github.com/actions/checkout
この記事を共有:XはてブLinkedIn