GitHub
Unity3D
Unity
Unity拡張
UnityEditor

【Unity】GitHubで空のフォルダ(空のディレクトリ)を管理できるエディタ拡張

概要

今回は、ただ置いておくだけで空のフォルダをGitHubで管理できるようにしてくれるエディタ拡張を作成しました。

空のフォルダの扱い

GitHubでUnityのプロジェクトを管理する際、空のフォルダの扱いは難しいです。

空のフォルダはコミットできません。しかし、metaファイルはコミットできてしまうので、そのあたりでしばしば競合が起こります。

例えば、「metaがあるのにフォルダがないよ!」とUnityに怒られたり、「metaが生成されたっぽい、コミットする?」とGitクライアントにしつこく聞かれたり。
特に複数人開発の場合、誰かがフォルダを新規作成してmetaファイルだけコミットしてしまったりでもう泥沼です。

GitHubで空のフォルダをコミットしたければ、「.gitkeep」なりダミーファイルを生成し、それを対象のフォルダに入れておくというのが通例のようです。

解決策

上記を踏まえ検索をかけると、エディタ拡張による解決策がいくつかヒットします。
アプローチは大きく分けて2つでした。

  1. 空のフォルダをすべて削除する
  2. フォルダを作ったときに「.gitkeep」を自動生成する

 1. は、そもそも空のフォルダは管理をしないと割り切って、自動ですべて削除してしまうという方法です。
確かに合理的な方法ではあるのですが、Unityのように画像やサウンドなどのリソースを多く扱うツールの場合、「後でここにファイルを置く」と明示的にフォルダを残したい場合が多いと思います。
したがって、この方法では「空のフォルダをGit管理したい」という目的は果たせません。

 2. は、フォルダを新規作成したらその中に「.gitkeep」を自動で配置するという方法です。
これも悪くはないのですが、のちのち新しいフォルダなりファイルなりを作成しても「.gitkeep」は勝手に消えてくれないため、「.gitkeep」が大量に生成されてしまうという懸念があります。また、エクスプローラーなどUnity外で新しいフォルダやファイルを追加するときには「.gitkeep」が生成されないので、一貫性がなくなってしまいます。
そのため、こちらも個人的に避けたい方法でした。

どちらの方法もデメリットがあり、GitHubで空のフォルダをきれいに管理するという解決には至りませんでした。

機能

というわけで、

  • Unity起動時
  • フォルダやスクリプト生成、素材インポートなどのアセット更新時
  • メニューから手動で実行時

にプロジェクト内の「.gitkeep」を一括で正しく配置(生成・削除)してくれるエディタ拡張を作りました。

使い方

  • FolderKeeper.csをAssets/Editorに入れてください
  • その時点でアセットの更新が入るので、「.gitkeep」が必要に応じて配置されます
  • あとは置いておけば、起動時、新規フォルダ作成時、新規ファイル作成時に「.gitkeep」が整理されます
  • 手動で再配置を実行したいときは、Unityのメニューから「Tool」→「Set Keepers」を選択してください

フォルダをたどっていく過程で、余計な「.gitkeep」があれば削除、必要なところには「.gitkeep」を生成してくれます。
新しくフォルダを作ったり、削除したときにも再整理がなされるため、常に適切な箇所に「.gitkeep」が置かれている状態を保つことができます。

FolderKeeper.cs
using UnityEditor;
using UnityEngine;
using System.IO;

//起動時に実行
[InitializeOnLoad]
public class FolderKeeper : AssetPostprocessor {

    //キーパーの名前
    public static readonly string keeperName = ".gitkeep";

    //コンストラクタ(起動時に呼び出される)
    static FolderKeeper() {
        //処理を呼び出す
        SetKeepers();
    }

    //アセット更新時に実行
    public static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetsPath) {
        SetKeepers();
    }

    //メニューにアイテムを追加
    [MenuItem("Tools/Set Keepers")]

    //呼び出す関数
    public static void SetKeepers() {

        //キーパーを配置する
        CheckKeeper("Assets");

        //データベースをリフレッシュする
        AssetDatabase.Refresh();

    }

    //キーパーを配置する関数
    public static void CheckKeeper(string path) {

        //ディレクトリパスの配列
        string[] directories = Directory.GetDirectories(path);
        //ファイルパスの配列
        string[] files = Directory.GetFiles(path);
        //キーパーの配列
        string[] keepers = Directory.GetFiles(path, keeperName);

        //ディレクトリがあるかどうか
        bool isDirectoryExist = 0 < directories.Length;
        //(キーパー以外の)ファイルがあるかどうか
        bool isFileExist = 0 < (files.Length - keepers.Length);
        //キーパーがあるかどうか
        bool isKeeperExist = 0 < keepers.Length;

        //ディレクトリもファイルもなかったら
        if (!isDirectoryExist && !isFileExist) {
            //キーパーがなかったら
            if (!isKeeperExist) {
                //キーパーを作成
                File.Create(path + "/" + keeperName).Close();
                Debug.Log("Keeper Created : " + path);
            }
            return;
        }
        //ディレクトリかファイルがあったら
        else {
            //キーパーがあったら
            if (isKeeperExist) {
                //キーパーを削除
                File.Delete(path + "/" + keeperName);
                Debug.Log("Keeper Deleted : " + path);
            }
        }

        //さらに深い階層を探索
        foreach (var directory in directories) {
            CheckKeeper(directory);
        }
    }
}

これですっきり。
チーム開発などでも役に立つかと思いますので、ご活用ください。
ご拝読、ありがとうございました。