見出し画像

Git LFSのやめ方

このたび全社的にGitHubが使えるようになったため、プロダクトで使っているGitリポジトリをBitBucketからGitHubへ移行しました。

契約や認証まわりはおんぶに抱っこで、私自身はリポジトリの移行自体に集中することが出来たのですが、その際 Git LFSをどうするかが大きなトピックの1つになりました。

GitHubでGit LFSを使うと、容量・通信の従量に応じて課金が発生します。
無料枠はあるものの、弊開発部のリポジトリでは全く賄いきれず重課金のおそれがあり、結果としてGit LFSをやめたうえで移行することになりました。

以下はその時の手順について書き起こしたものになります。
同じような境遇の方の参考になれば幸いです。

はじめに

Git LFSとは

Git Large File Storage の略で、音楽・動画・画像などサイズの大きいバイナリをGitリポジトリで管理するときに使うプラグインのようなものです。

Gitで修正をコミットすると 差分ではなく修正したファイル全体が保存され、cloneの際は作業ブランチと関係のない過去のコミットも併せてダウンロードされるため、大きいバイナリをGitに入れてしまうと利用時のサイズがどんどん膨れ上がってしまいます。

履歴に含まれるバイナリもコピーされる
(説明の都合上 ブランチとコミットの表現を曲げています)

Git LFS を使えばこの問題を回避できます。
指定した拡張子のファイルをポインタファイル(軽量なテキスト)に置き換えて、実ファイルは別サーバで管理します。
clone時は作業ブランチに含まれるポインタファイルのみサーバから実ファイルを取得して置換するため、サイズが肥大化することはありません。

必要な分だけバイナリに置換するので省スペース

元々利用していたBitBucket(オンプレ版)は社内のサーバということもありGit LFSは容量を気にせず使い放題でした。

GitHubとGit LFS

GitHubでもGit LFSが利用できます。ただし有償です。

Git LFSを使わないのであれば、Git LFSが管理している全ファイルをチェックして、Git LFSをやめるように履歴を改変する必要があります。

ただしGitHub側のGitの仕様として、100MB以上のファイルはそもそもコミットすることができきないようになっています(その場合はGit LFSを使う)。

なので履歴を改変する際は、100MB以上のものはGit LFSをやめるだけでなく、リポジトリから削除しなければいけません。

履歴の改変

準備

移行対象のリポジトリをベアリポジトリとしてcloneします。

git clone --bare (リポジトリのURL) migration.git
cd migration.git

ベアリポジトリは通常のclone結果からチェックアウトしたファイルを取り除いたもの、つまり.gitディレクトリほぼそのものになります(全履歴をfetchした状態)

ちなみにベアリポジトリからはcloneが可能です。
GitHub等でリポジトリを新規作成したときに作られるのがベアリポジトリなので当前ではあるのですが、このようにローカルのパスを指定してcloneすることもできます。

git clone ./migration.git work 

移行の作業では 元のリポジトリに対しては変更を行わず、代わりにcloneしたベアリポジトリ・またはcloneしたベアリポジトリからさらにcloneしたワーキングディレクトリに対して改変をpushしていきます。

改変が終わり次第、空のリポジトリを用意して そこにpushすることで作業の内容を保存します。
このときpush先をGitHubの空リポジトリにすると、そのままGitHubへ移すことも出来ます。

git push --mirror (空リポジトリのURL)

ファイルサイズの調査

100MBを超える、または今後超えそうなファイルを見つけるため、リポジトリ直下で次のコマンドを使います。

git lfs ls-files --debug --all

# 出力例
#
# filepath: a/b/c/d.png
#     size: 1037451
# checkout: false
# download: false
#      oid: sha256 2fb04d58619db5e3g254db55c7409d8e335b7a7f1925d22406a0f767b9cc7ebc
#  version: https://git-lfs.github.com/spec/v1
#
# ...(以下 繰り返し)...

Git LFSが管理している全履歴の全ファイルについて、ファイルパスやファイルサイズなどが出力されます。

この出力結果を加工して表にまとめるなどして、履歴から削除するもの・しないものを管理しておくと良いです。

改変1(Git LFSで管理されていなかったことにする)

まずすべてのファイルに対して、Git LFSによる管理をやめるよう履歴を改変します。

バイナリを直接配置するやり方に戻す
# すべての履歴に含まれるGitLFS管理ファイルをfetch
git lfs fetch --all

# すべてのポインタファイルを実ファイルに置き換える
git lfs migrate export --include '*' --everything

このとき、次のようなエラーが出て処理が止まることがあります。

[2d93...5a02] 対象がサーバー上に存在しません: [404] 対象がサーバー上に存在しません

ポインタファイルはあるものの、参照先の実ファイル(.git/lfs/objects/配下のファイル)がないためGit LFSのサーバと同期しようとして404エラーになるケースです。

過去に何らかの理由でGit LFSサーバへのPushが失敗していることが原因で、復旧用のファイル、それが用意できなければ空のファイルを適切な.git/lfs/objects/配下に置くことで処理が進みます。

# ハッシュ値に対応するパスに配置。長いので省略しています
touch ./lfs/objects/2d/93/2d93___5a02

改変2(履歴から削除)

100MBを超える、または超えそうなファイルを履歴から削除します。

※ 開発環境やビルドに影響を与えないように、ビルドスクリプトを通して外部から削除したバイナリを別途ダウンロードする等のケアが必要ですが、リポジトリ個別の話になるので立ち入りません

公式のfilter-branchコマンドを使うやり方もありますが、おすすめしません。
小さなリポジトリであれば問題ないですが、指定したコマンドをコミットの数だけ繰り返し実行する動きをするため、それなりに歴史があるリポジトリだといつまで立っても処理が終わりません。

# 全コミットから passwords.txt を削除
git filter-branch --tree-filter 'rm -f passwords.txt' HEAD

私は代わりに BFG というツールを使いました。
用途は削除に限定されていますが非常に高速です。

alias bfg='java -jar bfg-1.14.0.jar'
bfg --delete-files 'aaa.zip'
bfg --delete-files 'bbb.tar.gz'
bfg --delete-files '*.dat'
...

# 削除したいファイルの分だけ繰り返し

リポジトリの具体的な数字や条件が出せないのでお気持ち程度の話になりますが、手元ではfilter-branch1回1時間ほどかかっていた1ファイルの削除がBFGでは1分ほどで終わっています。

そんなBFGですが、HEAD(デフォルトブランチの最新コミット)に対して削除が実行できない点だけ注意が必要です。

HEAD以外の全履歴が対象

事前にHEADに対して追加のコミットを行い、該当ファイルを削除してからBFGを実行すれば大丈夫です。

cd ..
git clone ./migration.git work
cd work
# 該当ファイルを削除してcommit & push
cd ../migration.git

# BFG実行
ファイルを削除するコミットを追加してからbfgを実行

確認

無事LFSが使われなくなっていれば、.git/lfs 配下のサイズが0になっているはずです。

# reflog削除、GC
git reflog expire --expire=now --all && git gc --prune=now --aggressive

# 0byteになるはず
du -sh ./lfs

その他の注意点

  • .gitattributesについて、Git LFSの設定行のみ削除(git lfs untrack)すべきですが、BFGだとファイルの削除しかできないので .gitattributes のみfilter-branch で対応しても良いかもしれません。私は一旦BFGですべて削除したあと、主要ブランチのみビルドスクリプトの修正と併せてGit LFS 無効化済みの.gitattributesを追加する対応を行いました。

  • リポジトリ移行後は、チームメンバーに対して必ずリポジトリを clone し直してもらうよう依頼します。remote urlを書き換えるだけだと移行先のGit LFSサーバに不要なファイルが送信され、使っていないのに容量が消費される場合があります。

  • GitHubで意図せず Git LFSの容量を消費してしまったら、有償アカウント限定ですがサポートに問い合わせて削除を依頼できます

あとがき

 Git LFSの履歴改変はローカルにベアリポジトリを作って手軽に予行演習ができることもあり、当日は大きなトラブルもなく移行作業を終えることができました。

GitHubにリポジトリを移してから、社内サーバで回していたビルドやデプロイをGitHub Actions、GitHub Pagesに移行する動きができたりと、早速活用が始まっています。

リポジトリとビルドシステムの距離が近くなり、インフラに手をつけやすくなるのもGitHubの良いところだと感じました。

#エンジニア ​ #ITエンジニア​ #開発#ウイングアーク#ウイングアーク1st#テックブログ#エンジニア転職#エンジニア採用 #GitHub #GitLFS


この記事が気に入ったらサポートをしてみませんか?