C#で特定フォルダ内のgitignore対象ではないファイル/サブフォルダを一括で取得する

前回の記事「C#で特定のファイルorフォルダがgitignore対象かを判定する(単一ファイル編) - hnx8のブログ」では単一のファイル/フォルダがgitignore対象かどうかを調べる方法を紹介しました。
git check-ignore コマンドではコマンドパラメータ指定で複数のファイル等を指定してgitignore対象になっているかをまとめて調べることもできるので、特定ディレクトリ内の全ファイル/サブフォルダがgitignore対象かどうかを一括で判定するようなメソッドにしてみましょう。


git check-ignoreコマンドの実行結果 ですが、指定したファイル/フォルダのうちgitignore対象だったものについてパス名が標準出力へと出力されるようになっており、標準出力に含まれているフォルダ/ファイルがgitignore対象だと判定することができます。
また、調べたいファイル/フォルダは「--stdin」オプションを指定することで標準入力から(\n区切りで)まとめて指定することもできます。
今回は(対象パス数が多すぎた場合にコマンドパラメータの字数上限に引っかかる可能性もあるので)「--stdin」で対象を指定し、標準出力への出力結果はOutputDataReceived イベントで逐次受け取るするやりかたでソースコードを書くことにします。
DirectoryInfoに対する拡張メソッドとして定義してみました。
DirectoryInfo.GetFiles()DirectoryInfo.EnumerateFiles() などと同じような要領で利用できます。

/// <summary>
/// C#で.gitignore対象となっていないファイル等の一覧を取得するための拡張メソッド
/// </summary>
static class GitIgnoreExtension
{
    /// <summary>gitignore判定に使用するgit.exeのフルパス</summary>
    /// <remarks>仮に直値としてある。gitインストール先に応じた値を設定すること。</remarks>
    public static string GitExe_Path = @"C:\Program Files\Git\cmd\git.exe";

    /// <summary>
    /// そのディレクトリ直下で.gitignore対象に指定されていないファイルの一覧を返します。
    /// </summary>
    /// <param name="dir">ディレクトリ</param>
    /// <param name="searchPattern">検索パターン。 既定のパターンは "*" で、すべてのファイルが返されます。</param>
    /// <returns>searchPattern に一致するファイルの一覧。</returns>
    public static FileInfo[] GetNotIgnoredFiles(this DirectoryInfo dir, string searchPattern = "*")
    {
        return EnumerateNotIgnoredFiles(dir, searchPattern).ToArray();
    }

    /// <summary>
    /// そのディレクトリ直下で.gitignore対象に指定されていないファイルを列挙します。
    /// </summary>
    /// <param name="dir">ディレクトリ</param>
    /// <param name="searchPattern">検索パターン。 既定のパターンは "*" で、すべてのファイルが返されます。</param>
    /// <returns>searchPattern に一致するファイルの列挙。</returns>
    public static IEnumerable<FileInfo> EnumerateNotIgnoredFiles(this DirectoryInfo dir, string searchPattern = "*")
    {
        return EnumerateNotIgnoredFiles(dir, dir.GetFiles(searchPattern));
    }

    /// <summary>
    /// そのディレクトリ直下で.gitignore対象に指定されていないサブディレクトリを列挙します。
    /// </summary>
    /// <param name="dir">ディレクトリ</param>
    /// <param name="searchPattern">検索パターン。 既定のパターンは "*" で、すべてのディレクトリが返されます。</param>
    /// <returns>searchPattern に一致するディレクトリの列挙。</returns>
    public static IEnumerable<DirectoryInfo> EnumerateNotIgnoredDirectories(this DirectoryInfo dir, string searchPattern = "*")
    {
        return EnumerateNotIgnoredFiles(dir, dir.GetDirectories(searchPattern));
    }

    /// <summary>
    /// そのディレクトリ直下で.gitignore対象に指定されていないファイルおよびディレクトリを列挙します。
    /// </summary>
    /// <param name="dir">ディレクトリ</param>
    /// <param name="searchPattern">検索パターン。 既定のパターンは "*" で、すべてのファイルおよびディレクトリが返されます。</param>
    /// <returns>searchPattern に一致するファイルおよびディレクトリの列挙。</returns>
    public static IEnumerable<FileSystemInfo> EnumerateNotIgnoredFileSystemInfos(this DirectoryInfo dir, string searchPattern = "*")
    {
        return EnumerateNotIgnoredFiles(dir, dir.GetFileSystemInfos(searchPattern));
    }


    /// <summary>
    /// (private)引数で指定されたディレクトリ内のファイル等のうち、.gitignore対象になっていないものを列挙します。
    /// </summary>
    /// <typeparam name="T">FileInfo/DirectoryInfo/FileSystemInfoのいずれか</typeparam>
    /// <param name="dir">ディレクトリ</param>
    /// <param name="items">調べる対象のファイル等</param>
    /// <returns>.gitignore対象になっていないファイル等の列挙</returns>
    private static IEnumerable<T> EnumerateNotIgnoredFiles<T>(DirectoryInfo dir, IList<T> files)
        where T : FileSystemInfo
    {
        // git check-ignoreコマンドでgitignore対象となっているファイル等を調べる。対象はstdinから指定
        ProcessStartInfo psInfo = new ProcessStartInfo(GitExe_Path, "check-ignore --stdin")
        {
            CreateNoWindow = true,
            UseShellExecute = false,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            WorkingDirectory = dir.FullName,
        };
        using (Process p = Process.Start(psInfo))
        {
            // stdoutから無視すべきファイルの一覧をOutputDataReceivedイベントで受け取ってlistに蓄積
            List<string> ignoredNames = new List<string>();
            p.OutputDataReceived += (sender, e) =>
            {
                if (!string.IsNullOrEmpty(e.Data)) { ignoredNames.Add(e.Data); }
            };
            p.BeginOutputReadLine();
            // 調べる対象のファイルをすべてstdinで引き渡す
            using (StreamWriter sw = p.StandardInput)
            {
                foreach (T fi in files)
                {
                    sw.Write(fi.Name + "\n");
                }
            }
            // コマンド実行終了まで待ち、最悪でも10秒で強制終了
            if (!p.WaitForExit(10000))
            {
                p.Kill();
            }
            p.CancelOutputRead();
            // 無視すべきファイルの一覧に含まれていないものを列挙して返す
            foreach (T fi in files)
            {   // ※git管理外のディレクトリ等で戻り値が0でも1でもない場合も、無視すべきファイルとはみなさない
                if ((!ignoredNames.Contains(fi.Name) && fi.Name != ".git") || p.ExitCode > 1)
                {
                    yield return fi;
                }
            }
        }
    }
}

気になる git check-ignore コマンドの実行所要時間ですが、調べる対象のファイル/フォルダ数にかかわらず一定のようで、自分の環境ではコマンド実行1回あたりおおむね数十mSec程度となりました。