1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Git】100MBを超えるファイルのpushを未然に防ぐ

1
Posted at

はじめに

Unityでフォントアセットやその他アセットを扱うときに、以下のようなエラーに遭遇することがあります。

remote: error: File xxx.asset is 152.34 MB; this exceeds GitHub's file size limit of 100.00 MB

これはGitHubのファイル制限のエラーで、50MBを超えると警告が表示され、100MBを超えるとpushが拒否されます。さらに厄介なのは、一度コミットしてしまうと履歴に残るため、単純に削除してもpushエラーが解消されない点です。

なので今回はGitのpre-commit hookを使って、「100MB超のファイルが含まれるコミット」を作らせないような仕組みを考えました。

なお、本記事のスクリプトは生成AIを活用して作成しています。

動作環境

  • Windows 11
  • Git for Windows (Git Bash 同梱)

pre-commit hookとは?

Gitには「特定のタイミングで自動的にスクリプトを実行する」仕組みがあり、これをGit hookと呼びます。

その中でもpre-commitはコミットが作成される直前に実行されるhookで、ここで終了コード1を返すとコミットが中止されます。つまり、「条件を満たさない場合はコミットさせない」というガード処理が書けるわけです。

hookの実体は .git/hooks/ ディレクトリ配下のスクリプトファイルです。
hookスクリプトは、git initしたときやgit cloneしたときに、そのディレクトリ内の.git/hooks/内に生成されます1

your-repo/
├── .git/
│   └── hooks/
│       ├── pre-commit.sample
│       ├── pre-push.sample
│       └── ...
└── src/

.sample拡張子のファイルがサンプルとして用意されていますが、実行されるのは拡張子なしのpre-commitというファイルです。

手順

既に何らかのリポジトリをcloneしている前提で話を進めます。

1. 対象リポジトリに移動

Git Bashを起動して、設定したいリポジトリのルートに移動します。

cd /c/Users/your-name/projects/your-repo

.git/hooks/内にいろんなhookスクリプトのsampleがあるのを確認します。

ls .git/hooks/
applypatch-msg.sample
commit-msg.sample
fsmonitor-watchman.sample
post-update.sample
pre-applypatch.sample
pre-commit.sample
pre-merge-commit.sample
...

2. pre-commitファイルを作成

pre-commitファイルを作成します。エクスプローラー等でファイルを作っても大丈夫です。

touch .git/hooks/pre-commit

image.png

3. スクリプトを記述

作成したファイルをエディタで開いて、以下の内容を貼り付けます。

#!/bin/sh
# 100MBを超えるファイルがステージングされていないかチェックするhook

# 上限サイズ (100MB = 100 * 1024 * 1024 bytes)
MAX_SIZE=104857600
EXIT_CODE=0

# ステージング対象のファイル一覧を取得 (Added/Copied/Modifiedのみ)
files=$(git diff --cached --name-only --diff-filter=ACM)

# ステージングされたファイルが無い場合はスキップ
if [ -z "$files" ]; then
    exit 0
fi

# 1ファイルずつサイズを確認
while IFS= read -r file; do
    if [ -f "$file" ]; then
        size=$(wc -c < "$file")
        if [ "$size" -gt "$MAX_SIZE" ]; then
            size_mb=$(awk "BEGIN {printf \"%.2f\", $size / 1048576}")
            echo "$file (${size_mb}MB) は100MBを超えています"
            EXIT_CODE=1
        fi
    fi
done <<< "$files"

if [ "$EXIT_CODE" -ne 0 ]; then
    echo ""
    echo "コミットを中止しました。"
    echo "GitHubには100MBを超えるファイルをpushできません。"
    echo ""
fi

exit $EXIT_CODE

4. 実行権限を付与

シェルスクリプトとして実行できるよう、実行権限を付けます。

chmod +x .git/hooks/pre-commit

スクリプトの中身を理解する

スクリプトは大きく3つのブロックに分かれています。

ステージング対象が無ければ何もしない

if [ -z "$files" ]; then
    exit 0
fi

$filesが空文字列かどうかをチェック (-zは「文字列が空ならtrue」)し、空であれば exit 0で正常終了します。

git commit --amendでメッセージだけ修正したい時など、ステージング対象のファイルが0件のケースがあります。このチェックを入れずに先のループに進むと、空の入力に対して処理が走ってしまうため、最初の段階で「対象なし」を判定して抜けています。

exit 0は正常終了を意味するので、コミットはそのまま続行されます。

1ファイルずつサイズを判定する

while IFS= read -r file; do
    if [ -f "$file" ]; then
        size=$(wc -c < "$file")
        if [ "$size" -gt "$MAX_SIZE" ]; then
            size_mb=$(awk "BEGIN {printf \"%.2f\", $size / 1048576}")
            echo "$file (${size_mb}MB) は100MBを超えています"
            EXIT_CODE=1
        fi
    fi
done <<< "$files"

このブロックがhookの心臓部です。1行ずつ見ていきます。

while IFS= read -r file; do ... done <<< "$files"

$filesの中身を 1行ずつ 読み込んで、変数fileに代入しながらループします。

  • IFS= : 行頭・行末の空白が削られないようにする設定
  • read -r : バックスラッシュをエスケープとして解釈しない(ファイル名をそのまま扱う)
  • <<< "$files" : 文字列を標準入力として渡す書き方

if [ -f "$file" ]; then

そのパスが実在するファイルかを確認しています (-f は「通常ファイルが存在すればtrue」)。シンボリックリンクやディレクトリ、削除済みファイルを除外する安全装置です。

size=\$(wc -c < "$file")

ファイルのバイト数を取得します。wcは文字数カウントコマンドですが、-cオプションでバイト数を返します。

if [ "\$size" -gt "$MAX_SIZE" ]; then

-gtは、greater than(より大きい)の意味で、取得したサイズと上限値(104857600バイト = 100MB)を比較しています。

size_mb=$(awk "BEGIN {printf "%.2f", $size / 1048576}")

エラーメッセージ表示用に、バイト数をMB単位に変換しています。awkを使っているのは、シェルの標準機能では小数点の計算ができないためです。1048576は、1024 × 1024(=1MB のバイト数)です。例: 157286400 / 1048576 = 150.00 MB

EXIT_CODE=1

100MB超のファイルが見つかった時点で、終了コードを1(エラー)にセットします。ただし、ループはここで止めずに最後まで回し、最後に100MB超のファイルをまとめて報告します。

エラーメッセージの出力

if [ "$EXIT_CODE" -ne 0 ]; then
    echo ""
    echo "コミットを中止しました。"
    echo "GitHubには100MBを超えるファイルをpushできません。"
    echo ""
fi

exit $EXIT_CODE

-neは not equal(等しくない)の意味で、EXIT_CODEが0でない(=サイズ超過があった)場合のみメッセージを表示するようにしています。

そして最後に書かれているexit $EXIT_CODEで、Gitに対して「このコミットは中止」を伝えます。

ここまでで、100MBを超えるファイルを含んだコミットをブロック出来るようになりました。
ここからはおまけとして、100MBを超えるファイルを.gitignoreに追加して、push出来るようにすることを考えます。

おまけ:100MB超ファイルを.gitignoreに追加してpush出来るようにする

hookで弾かれた時点だと、ファイルはgit add済み(ステージング状態)になっています。まずはステージングから外しましょう。なお、今回は未トラッキング(まだ一度もコミットされていない)のファイルを対象としています。

git restore --staged "large file.zip"

git statusで確認して、Untracked filesのセクションに表示されていればOKです。

git status
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        large file.zip

.gitignoreをエディタで開いて、対象ファイルのパスを追記します。

large file.zip

git statusで、先ほどまで表示されていたlarge file.zipが一覧から消えていれば成功です。これ以降はaddをしてもlarge file.zipはステージング対象になりません。

git status
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   .gitignore

最後に、編集した.gitignoreの内容をcommitします。

git add .gitignore
git commit -m "chore: 大容量ファイルを.gitignoreに追加"

これで、100MBを超えるファイルはコミットに含まれなくなったので、問題なくpush出来るようになります。

まとめ

pre-commit hookを使って、100MBを超えるファイルが含まれるコミットを未然に防ぐ仕組みを構築しました。

  • .git/hooks/pre-commitにスクリプトを置くだけで導入可能
  • 実体はシェルスクリプトなので、用途に応じてカスタマイズしやすい
  • .gitignoreと組み合わせれば、大容量ファイルを除外したうえで安全にpushできる

今回はサイズチェックのみ実装しましたが、pre-commit hookは他にも以下のような用途で活用できます。

  • コミットメッセージの形式チェック
  • Lintやテストの自動実行
  • 機密情報(APIキーなど)の混入チェック

「コミット前の自動チェック」は開発体験を大きく改善する仕組みです。今回の記事をきっかけに、ぜひ自分の開発フローに合ったhookを作ってみてください!

  1. https://qiita.com/noraworld/items/c562de68a627ae792c6c

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?